I would like to rearrange UICollectionView cells during the rotation of my device in a similar way this is done in the evernote iPads app for the notes. In the default implementation there is just a fade-in and fade-out of the cells but i would like to have the cells to move around during the rotation.
What would be the recommended way to achieve a similar animation? Do I need to create a custom UICollectionViewLayout?
I managed to get the desired rotation effect by subclassing UICollectionViewFlowLayout and overriding the two methods: initialLayoutAttributesForAppearingItemAtIndexPath and finalLayoutAttributesForDisappearingItemAtIndexPath which are the two points of controls to respectively define the starting/final layout information for an item being inserted into the collection view.
See source code:
.h:
#import "UICollectionView.h"
#interface UITestCollectionViewFlowLayout : UICollectionViewFlowLayout
#end
.m:
#import "UITestCollectionViewFlowLayout.h"
#interface UITestCollectionViewFlowLayout ()
{
BOOL _isRotating;
}
#property (strong, nonatomic) NSIndexPath* lastDissappearingItemIndex;
#end
#implementation UITestCollectionViewFlowLayout
#synthesize lastDissappearingItemIndex = _lastDissappearingItemIndex;
// returns the starting layout information for an item being inserted into the collection view
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes* attributes = (UICollectionViewLayoutAttributes *)[self layoutAttributesForItemAtIndexPath:itemIndexPath];
if (_isRotating) // we want to customize the cells layout only during the rotation event
{
if ([self.lastDissappearingItemIndex isEqual:itemIndexPath])
return nil; // do not animate appearing cell for the one that just dissapear
else
{
attributes.alpha = 0;
// setting the alpha to the new cells that didn't match the ones dissapearing is not enough to not see them so we offset them
attributes.center = CGPointMake(attributes.center.x, attributes.size.height * 2 + attributes.center.y);
}
}
return attributes;
}
// returns the final layout information for an item that is about to be removed from the collection view
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath*)itemIndexPath
{
UICollectionViewLayoutAttributes* attributes = (UICollectionViewLayoutAttributes *)[self layoutAttributesForItemAtIndexPath:itemIndexPath];
if (_isRotating)
{
attributes.alpha = 1.0;
self.lastDissappearingItemIndex = itemIndexPath;
}
return attributes;
}
- (void) viewController:(UIViewController *)viewController didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
_isRotating = NO;
}
- (void) viewController:(UIViewController *)viewController willRotateToInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration
{
_isRotating = YES;
}
Note that i am using a flag that i set from the viewcontroller who create this flowlayout to know when we are actually rotating or not; the reason being that i want this effect to happen only during rotation.
I am interested to hear feedbacks/improvement about this code.
Related
I'm going to create a book shelf in one of my projects. The basic requirements are as follows:
similar to iBooks bookshelf
supports both orientations well
supports all kinds of iOS devices well (different resolutions)
supports deleting and inserting items
supports reordering of items by long press gesture
shows a hidden logo when first row is pulled down
The UICollectionView is the first option that occurred to me. It easily supports grid cells. So I googled it and found some very useful tutorials:
Bryan Hansen's UICollectionView custom layout tutorial
Mark Pospesel's How to Add a Decoration View to a UICollectionView
LXReorderableCollectionViewFlowLayout
And here is the result: (Please ignore the color inconsistency problem because of the graphics I chose are not perfect yet.)
What I did:
Created a custom layout by creating a class inherited from LXReorderableCollectionViewFlowLayout (for reordering purposes) which inherited from UICollectionFlowLayout
Added a decoration view for showing logo
Added a decoration view for showing the bookshelfs
But I ran into a few problems:
1. I can't scroll at all if the items can be shown in one screen
Then I added the following code, to make the contentsize bigger
- (CGSize)collectionViewContentSize
{
return CGSizeMake(self.collectionView.bounds.size.width, self.collectionView.bounds.size.height+100);
}
Then I can scroll now. Here I pull down the first row:
You can see the the decoration view for logo is working.
2. But I got the second set of problems when I pull up the last row:
You can see the decoration view is not added at the green box part.
3. The background of decoration view for bookshelf is getting darker and darker. (Please refer to same problem here
4. The book shelf bar sometimes moves when I reorder the items
I list some of the important code here:
- (void)prepareLayout
{
// call super so flow layout can do all the math for cells, headers, and footers
[super prepareLayout];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
NSMutableDictionary *shelfLayoutInfo = [NSMutableDictionary dictionary];
// decoration view - emblem
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
UICollectionViewLayoutAttributes *emblemAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:[EmblemView kind]
withIndexPath:indexPath];
emblemAttributes.frame = [self frameForEmblem:YES];
dictionary[[EmblemView kind]] = #{indexPath: emblemAttributes};
// Calculate where shelves go in a vertical layout
int sectionCount = [self.collectionView numberOfSections];
CGFloat y = 0;
CGFloat availableWidth = self.collectionViewContentSize.width - (self.sectionInset.left + self.sectionInset.right);
int itemsAcross = floorf((availableWidth + self.minimumInteritemSpacing) / (self.itemSize.width + self.minimumInteritemSpacing));
for (int section = 0; section < sectionCount; section++)
{
y += self.headerReferenceSize.height;
//y += self.sectionInset.top;
int itemCount = [self.collectionView numberOfItemsInSection:section];
int rows = ceilf(itemCount/(float)itemsAcross)+1; // add 2 more empty row which doesn't have any data
for (int row = 0; row < rows; row++)
{
indexPath = [NSIndexPath indexPathForItem:row inSection:section];
shelfLayoutInfo[indexPath] = [NSValue valueWithCGRect:CGRectMake(0,y, self.collectionViewContentSize.width, self.itemSize.height + DECORATION_HEIGHT)];
y += self.itemSize.height;
if (row < rows - 1)
y += self.minimumLineSpacing;
}
y += self.sectionInset.bottom;
y += self.footerReferenceSize.height;
}
dictionary[[ShelfView kind]] = shelfLayoutInfo;
self.shelfLayoutInfo = dictionary;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributesArrayInRect = [super layoutAttributesForElementsInRect:rect];
// cell layout info
for (BookShelfLayoutAttributes *attribs in attributesArrayInRect)
{
attribs.zIndex = 1;
CATransform3D t = CATransform3DIdentity;
t = CATransform3DTranslate(t, 0, 0, 40);
attribs.transform3D = CATransform3DRotate(t, 15 * M_PI / 180, 1, 0, 0);
}
// Add our decoration views (shelves)
NSMutableDictionary* shelfDictionary = self.shelfLayoutInfo[[ShelfView kind]];
NSMutableArray *newArray = [attributesArrayInRect mutableCopy];
[shelfDictionary enumerateKeysAndObjectsUsingBlock:^(id key, NSValue* obj, BOOL *stop) {
if (CGRectIntersectsRect([obj CGRectValue], rect))
{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:[ShelfView kind] withIndexPath:key];
attributes.frame = [obj CGRectValue];
NSLog(#"decorationView rect = %#",NSStringFromCGRect(attributes.frame));
attributes.zIndex = 0;
//attributes.alpha = 0.5; // screenshots
[newArray addObject:attributes];
}
}];
attributesArrayInRect = [NSArray arrayWithArray:newArray];
NSMutableDictionary* emblemDictionary = self.shelfLayoutInfo[[EmblemView kind]];
NSMutableArray *newArray2 = [attributesArrayInRect mutableCopy];
[emblemDictionary enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *innerStop) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[newArray2 addObject:attributes];
}
}];
attributesArrayInRect = [NSArray arrayWithArray:newArray2];
return attributesArrayInRect;
}
I'll appreciate if you're patient enough to read this post and provide any advice or suggestions. And I'll post the complete source code if I can fix all the issues. Thank you in advance.
I would suggest to check your last row and do the [self.collectionView ScrollEnable:NO] same for first row, set background color clear of collectionView and collectionViewCell and that bookshelf image sets on background view.
I have a UIImageView embedded inside a UIScrollView, prior to iOS 6 and autolayout I used the following snippet inside the controller's viedDidLoad method to display a scrollable and zoomable image.
self.scrollView.contentSize = self.imageView.image.size;
self.imageView.frame = CGRectMake(0, 0, self.imageView.image.size.width, self.imageView.image.size.height);
But now the constraints set in the storyboard are used instead. I've found this question Embed ImageView in ScrollView with Auto Layout on iOS 6 and some others here in SO stating that constraints are loaded/enforced after viewDidLoad, and that moving my previous snippet to viewDidAppear would fix this issue but zooming does not work properly and it seems that the sizes of the scrollView and the imageView are reset to the storyboard's constraint after a pinch-to-zoom gesture.
I'm just guessing, but I think maybe if there's some way to override the scrollView's and imageView's vertical and horizontal space constraints in code that might work.
Anyone else having this issues?
The best solution was proposed by Zsolt in the comments:
http://github.com/evgenyneu/ios-imagescroll check this out. Works perfectly for me.
The solution proposed in this repository is to adjust the minimum and current zoom level before displaying the image:
#interface MyViewController () <UIScrollViewDelegate>
#property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
#property (weak, nonatomic) IBOutlet UIImageView *imageView;
#end
#implementation MyViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.scrollView.delegate = self;
[self initZoom];
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self initZoom];
}
// Zoom to show as much image as possible
- (void) initZoom {
float minZoom = MIN(self.view.bounds.size.width / self.imageView.image.size.width,
self.view.bounds.size.height / self.imageView.image.size.height);
if (minZoom > 1) return;
self.scrollView.minimumZoomScale = minZoom;
self.scrollView.zoomScale = minZoom;
}
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
However sample conatins issue with zooming out. Image is not getting centered. This can be easily fixed by using the custom scroll view class with the following code in it:
#interface MyScrollView : UIScrollView
#end
#implementation MyScrollView
-(void)layoutSubviews
{
[super layoutSubviews];
UIView* v = [self.delegate viewForZoomingInScrollView:self];
CGFloat svw = self.bounds.size.width;
CGFloat svh = self.bounds.size.height;
CGFloat vw = v.frame.size.width;
CGFloat vh = v.frame.size.height;
CGRect f = v.frame;
if (vw < svw)
f.origin.x = (svw - vw) / 2.0;
else
f.origin.x = 0;
if (vh < svh)
f.origin.y = (svh - vh) / 2.0;
else
f.origin.y = 0;
v.frame = f;
}
#end
Solved my problem using the following code sample. The github repository corresponds to the book Programming iOS by Matt Neuburg.
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/11c6c57743b04e6e722b635b87be69fa41a5abaf/ch20p573scrollViewAutoLayout/ch20p573scrollViewAutoLayout/ViewController.m
I also agree with Zsolt's suggestion and link.
But I update the width/height constraints to allow it to handle any size image:
- (void) initZoom
{
for (NSLayoutConstraint *constraint in self.photoImageView.constraints)
{
if (constraint.firstAttribute == NSLayoutAttributeWidth)
constraint.constant = self.photoImageView.image.size.width;
else if (constraint.firstAttribute == NSLayoutAttributeHeight)
constraint.constant = self.photoImageView.image.size.height;
}
float minZoom = MIN(self.scrollView.bounds.size.width / self.photoImageView.image.size.width,
self.scrollView.bounds.size.height / self.photoImageView.image.size.height);
if (minZoom > 1) return;
self.scrollView.minimumZoomScale = minZoom;
self.scrollView.zoomScale = minZoom;
}
First, apologies if the answer is quite obvious but I'm brand new to iOS development (like this is the first app I'm trying to program even though it's just for me to play with :P) so the probability that my problem is rather minor is quite high.
Code (zip file containing the xcode project):
http://www.mediafire.com/?p55xw0q2dwwwwvm
I promise there's nothing else in there that's harmful :).
Problem:
I'm following:
http://www.techotopia.com/index.php/An_iPhone_iOS_6_Storyboard-based_Collection_View_Tutorial
I'm attempting to make a UICollectionView to play around with. I got down to the "Testing the Application" section. I put my own spin on a few parts from the guide:
instead of using different pictures, I just use 1 picture with a label at the bottom that just has a number in it. The number is dynamically set. Yes, I could implement it without the array I'm using, but having that array there will be helpful if I extend this project's scope in the future.
I added code in viewDidLoad to set the view layout to a flow layout. This was done again for futureproofing since once I get this working at a basic form I want to play around with formatting which will require me to subclass flowlayout.
The code compiles without errors but nothing shows up on the screen. I've checked the code to the best of my ability for about an hour or two, but nothing I did made any difference. My controller class is a bare bones attempt to just get the collection view to show up on screen, and my cell class is a bare bones cell with just an imageview and label in it.
Thanks for any help that can be given to get this working!
tl;dr
Just look at the bolded stuff. Code provided below for convenience:
MyCollectionViewController.m
#import "MyCollectionViewController.h"
#interface MyCollectionViewController ()
#property (nonatomic, strong) NSMutableArray *dataArray;
#end
#implementation MyCollectionViewController
#synthesize dataArray = _dataArray;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Create data to display
for(int i = 0; i < 50; i++){
[self.dataArray addObject:[NSString stringWithFormat:#"%d", i]];
}
// Configure Layout
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
[self.collectionView setCollectionViewLayout:flowLayout];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark -
#pragma mark UICollectionViewDataSource
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
-(NSInteger)collectionView:(UICollectionView*)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.dataArray count];
}
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCollectionViewCell *myCell = [collectionView dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
int row = [indexPath row];
UIImage* image = [UIImage imageNamed:#"200px-AND_ANSI"];
myCell.cellTitle = [self.dataArray objectAtIndex:row];
myCell.cellImageView.image = image;
myCell.backgroundColor = UIColor.whiteColor;
return myCell;
}
MyCollectionViewcontroller.h
#import <UIKit/UIKit.h>
#import "MyCollectionViewCell.h"
#interface MyCollectionViewController : UICollectionViewController <UICollectionViewDataSource, UICollectionViewDelegate>
#end
MyCollectionViewCell.h
#import <UIKit/UIKit.h>
#interface MyCollectionViewCell : UICollectionViewCell
#property (strong, nonatomic) IBOutlet UIImageView *cellImageView;
#property (strong, nonatomic) IBOutlet UILabel *cellTitle;
#end
MyCollectionViewCell.m
#import "MyCollectionViewCell.h"
#implementation MyCollectionViewCell
#synthesize cellImageView = _cellimageView;
#synthesize cellTitle = _cellTitle;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
#end
self.dataArray is nowhere allocated/initialized in your code and therefore is equal to nil. Sending messages to nil is allowed, but has no effect, therefore even after
for(int i = 0; i < 50; i++){
[self.dataArray addObject:[NSString stringWithFormat:#"%d", i]];
}
self.dataArray is still nil, and [self.dataArray count] returns 0.
You have to allocate and initialize the array with
self.dataArray = [[NSMutableArray alloc] init];
A proper place is some initXXX method of the view controller. But initWithNibName: is not called if the view controller is instantiated from a storyboard file, you have to use initWithCoder: instead:
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
self.dataArray = [NSMutableArray array];
}
return self;
}
I have a WPF Canvas with some Ellipse objects on it (displayed as circles). Each circle is from a collection class instance which is actually a custom hole pattern class. Each pattern has a certain number of circles, and each circle then gets added to the canvas using an iteration over the collection using the code below.
So, the canvas is populated with a bunch of circles and each circle belongs to a certain pattern instance. You can see a screenshot here: http://twitpic.com/1f2ci/full
Now I want to add the ability to click on a circle on the canvas, and be able to determine the collection it belongs to, so that I can then do some more work on the selected pattern to which that circle belongs.
public void DrawHoles()
{
// Iterate over each HolePattern in the HolePatterns collection...
foreach (HolePattern HolePattern in HolePatterns)
{
// Now iterate over each Hole in the HoleList of the current HolePattern...
// This code adds the HoleEntity, HoleDecorator, and HoleLabel to the canvas
foreach (Hole Hole in HolePattern.HoleList)
{
Hole.CanvasX = SketchX0 + (Hole.AbsX * _ZoomScale);
Hole.CanvasY = SketchY0 - (Hole.AbsY * _ZoomScale);
canvas1.Children.Add(Hole.HoleEntity);
}
}
}
All FrameworkElements have a Tag property which is of type object that can be used to hold arbitrary information. You could assign the HolePattern to the Tag property and easily use that later to get the associated collection.
i.e.:
...
Hole.HoleEntity.Tag = HolePattern as object;
canvas1.Children.Add(Hole.HoleEntity);
later on in the click event:
event(object sender,....)
{
Ellipse e = sender as Ellipse;
HolePattern hp = e.Tag as HolePattern;
...
}
So you probably already read my reply where I said I had it working. And it does work perfectly, (except that it requires great precision with the mouse), but I want to ask this: is it really smart to add an event handler to EVERY ellipse that gets added to a canvas? Now I don't know what kind of memory bog that could be, or maybe it is a piece of cake for WPF and Windows to handle.
In a practical case, I guess there would be not more that 30-50 holes even on a screen that had multiple patterns, but still; FIFTY event handlers? It just seems scary. And actually, each "Hole" is visually represented by two concentric circles and a text label (see the screenshow here: http://twitpic.com/1f2ci/full ), and I know the user would expect to be able to click on any one of those elements to select a hole. That means an event handler on 3 elements for every hole. Now we could be talking about 100 or more event handlers.
It seems like there should be a solution where you could have just one event handler on the Canvas and read the element reference under the mouse, then work off of that to get the .Tag property of that elment, and so on.
I thought I'd post my final and more refined solution in case it helps anyone else.
void canvas1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
int ClickMargin = 2;// Adjust here as desired. Span is in both directions of selected point.
var ClickMarginPointList = new Collection<Point>();
Point ClickedPoint = e.GetPosition(canvas1);
Point ClickMarginPoint=new Point();
for (int x = -1 * ClickMargin; x <= ClickMargin; x++)
{
for (int y = -1 * ClickMargin; y <= ClickMargin; y++)
{
ClickMarginPoint.X = ClickedPoint.X + x;
ClickMarginPoint.Y = ClickedPoint.Y + y;
ClickMarginPointList.Add(ClickMarginPoint);
}
}
foreach (Point p in ClickMarginPointList)
{
HitTestResult SelectedCanvasItem = System.Windows.Media.VisualTreeHelper.HitTest(canvas1, p);
if (SelectedCanvasItem.VisualHit.GetType().BaseType == typeof(Shape))
{
var SelectedShapeTag = SelectedCanvasItem.VisualHit.GetValue(Shape.TagProperty);
if (SelectedShapeTag!=null && SelectedShapeTag.GetType().BaseType == typeof(Hole))
{
Hole SelectedHole = (Hole)SelectedShapeTag;
SetActivePattern(SelectedHole.ParentPattern);
SelectedHole.ParentPattern.CurrentHole = SelectedHole;
return; //Get out, we're done.
}
}
}
}
I simply have two grid on top of one another. Given one state of the world, I want grid A to be on top, given another state of the world, I want grid B to be on top. In the old days we could just call grid.BringToFront(), but that doesn't exist anymore, and I can't figure out any way to make that happen.
The best I can figure, I need to create my own custom classes to allow this functionality, but that seems like major overkill for something that used to be so simple.
You can use the Panel.ZIndex property to change the display order of elements in a panel
You have to use the Z index property, and because there are no built-in function to do what you want, I made my own.
The higher the Z value, the 'closer' to front the control is.
So you want to put your control on top without having to set an arbitrary high Z value.
So here is a small function I wrote for myself to do exactly that.
Note: this assume that you are using a Canvas and UserControls.
So you might need to adapt it a little bit if that's not your case.
Basically it will get the index of the control to move, then any control currently above it will go down by 1 and the control to move will be put on top (to maintain hierarchy).
static public void BringToFront(Canvas pParent, UserControl pToMove)
{
try
{
int currentIndex = Canvas.GetZIndex(pToMove);
int zIndex = 0;
int maxZ = 0;
UserControl child;
for (int i = 0; i < pParent.Children.Count; i++)
{
if (pParent.Children[i] is UserControl &&
pParent.Children[i] != pToMove)
{
child = pParent.Children[i] as UserControl;
zIndex = Canvas.GetZIndex(child);
maxZ = Math.Max(maxZ, zIndex);
if (zIndex > currentIndex)
{
Canvas.SetZIndex(child, zIndex - 1);
}
}
}
Canvas.SetZIndex(pToMove, maxZ);
}
catch (Exception ex)
{
}
}
To whom it may concern:
ZIndex property is 0 by default, so if you have (like me) a Canvas with more than 1 element (>4000 Shapes in my case), all will have ZIndex = 0, so changing the ZIndexes with this method will have no effect.
For this to work, I set the ZIndexes to a known value after creating all the elements, so they can be ordered after.
int zIndex = 0;
for (int i = 0; i < canvas.Children.Count; i++) {
UIElement child = canvas.Children[i] as UIElement;
if (canvas.Children[i] is UIElement) Canvas.SetZIndex(child, zIndex++);
}
Instead of stacking the two grids, change the visibility properties so the grid you aren't using is collapsed.
Expanding on the answer from #PicMickael, this will do exactly as they described but with less instructions:
public void BringToFront<T>(T uiElement, Canvas canvas)
{
try
{
foreach (UIElement s in canvas.Children)
{
Canvas.SetZIndex(s, 1);
}
Canvas.SetZIndex(uiElement as UIElement, 2);
}
catch (Exception ex)
{
WriteLog.Error(ex);
}
}