├── FishEyeDemo ├── en.lproj │ └── InfoPlist.strings ├── MCSViewController.h ├── MCSDemoXibFishEyeViewItem.m ├── MCSAppDelegate.h ├── MCSDemoXibFishEyeViewItem.h ├── FishEyeDemo-Prefix.pch ├── main.m ├── MCSDemoFishEyeItem.h ├── MCSAppDelegate.m ├── FishEyeDemo-Info.plist ├── MCSDemoFishEyeItem.m ├── MCSViewController.m ├── MCSDemoXibFishEyeViewItem.xib └── MCSViewController.xib ├── Screens ├── collapsed.png ├── fishEye.gif ├── selected.png └── highlighted.png ├── FishEyeDemo.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── FishEyeDemo.xccheckout └── project.pbxproj ├── LICENSE ├── MCSFishEyeView.podspec ├── MCSFishEyeView ├── MCSFishEyeViewItem.m ├── MCSFishEyeViewItem.h ├── MCSFishEyeView.h └── MCSFishEyeView.m └── README.md /FishEyeDemo/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Screens/collapsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macoscope/MCSFishEye/HEAD/Screens/collapsed.png -------------------------------------------------------------------------------- /Screens/fishEye.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macoscope/MCSFishEye/HEAD/Screens/fishEye.gif -------------------------------------------------------------------------------- /Screens/selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macoscope/MCSFishEye/HEAD/Screens/selected.png -------------------------------------------------------------------------------- /Screens/highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macoscope/MCSFishEye/HEAD/Screens/highlighted.png -------------------------------------------------------------------------------- /FishEyeDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCSViewController.h 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 8/30/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MCSViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSDemoXibFishEyeViewItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCSDemoXibFishEyeViewItem.m 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 9/3/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import "MCSDemoXibFishEyeViewItem.h" 10 | 11 | @implementation MCSDemoXibFishEyeViewItem 12 | 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCSAppDelegate.h 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 8/29/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MCSAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSDemoXibFishEyeViewItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCSDemoXibFishEyeViewItem.h 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 9/3/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import "MCSFishEyeViewItem.h" 10 | 11 | @interface MCSDemoXibFishEyeViewItem : MCSFishEyeViewItem 12 | 13 | @property (weak, nonatomic) IBOutlet UILabel *label; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /FishEyeDemo/FishEyeDemo-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_5_0 10 | # warning "This project uses features only available in iOS SDK 5.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | # import 15 | # import 16 | #endif 17 | -------------------------------------------------------------------------------- /FishEyeDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 8/29/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "MCSAppDelegate.h" 12 | 13 | int main(int argc, char * argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([MCSAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSDemoFishEyeItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCSDemoFishEyeItem.h 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 8/30/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import "MCSFishEyeViewItem.h" 10 | 11 | @interface MCSDemoFishEyeItem : MCSFishEyeViewItem 12 | 13 | @property (nonatomic, strong, readonly) UIView *backgroundView; 14 | @property (nonatomic, strong, readonly) UILabel *label; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Macoscope, sp z o.o. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /MCSFishEyeView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "MCSFishEyeView" 3 | s.version = "1.0" 4 | s.summary = "The fisheye from Bubble Browser for iPad." 5 | s.homepage = "https://github.com/macoscope/MCSFishEyeView" 6 | s.license = { :type => 'MIT', :file => 'LICENSE.txt' } 7 | s.author = { "Bartosz Ciechanowski" => "ciechan@gmail.com" } 8 | s.source = { :git => "https://github.com/macoscope/MCSFishEyeView.git", :tag => "1.0" } 9 | s.platform = :ios 10 | s.source_files = 'FishEyeView/*.{h,m}' 11 | s.requires_arc = true 12 | s.frameworks = 'QuartzCore', 'CoreGraphics' 13 | s.ios.deployment_target = '5.0' 14 | end 15 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCSAppDelegate.m 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 8/29/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import "MCSAppDelegate.h" 10 | #import "MCSViewController.h" 11 | 12 | @implementation MCSAppDelegate 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 15 | { 16 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 17 | 18 | MCSViewController *viewController = [[MCSViewController alloc] init]; 19 | [self.window setRootViewController:viewController]; 20 | [self.window makeKeyAndVisible]; 21 | 22 | return YES; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /MCSFishEyeView/MCSFishEyeViewItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCSFishEyeViewItem.m 3 | // 4 | // Created by Bartosz Ciechanowski on 8/29/13. 5 | // Copyright (c) 2013 Macoscope. All rights reserved. 6 | // 7 | 8 | #import "MCSFishEyeViewItem.h" 9 | 10 | @implementation MCSFishEyeViewItem 11 | 12 | 13 | - (void)setSelected:(BOOL)selected 14 | { 15 | [self setSelected:selected animated:NO]; 16 | } 17 | 18 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated 19 | { 20 | _selected = selected; 21 | } 22 | 23 | - (void)setHighlighted:(BOOL)highlighted 24 | { 25 | [self setHighlighted:highlighted animated:NO]; 26 | } 27 | 28 | - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated 29 | { 30 | _highlighted = highlighted; 31 | } 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /MCSFishEyeView/MCSFishEyeViewItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCSFishEyeViewItem.h 3 | // 4 | // Created by Bartosz Ciechanowski on 8/29/13. 5 | // Copyright (c) 2013 Macoscope. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | // for the sake of pre iOS7 SDK compatibility 11 | #ifndef NS_REQUIRES_SUPER 12 | #if __has_attribute(objc_requires_super) 13 | #define NS_REQUIRES_SUPER __attribute__((objc_requires_super)) 14 | #else 15 | #define NS_REQUIRES_SUPER 16 | #endif 17 | #endif 18 | 19 | 20 | @interface MCSFishEyeViewItem : UIView 21 | 22 | @property (nonatomic, getter=isSelected) BOOL selected; 23 | @property (nonatomic, getter=isHighlighted) BOOL highlighted; 24 | 25 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated NS_REQUIRES_SUPER; 26 | - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated NS_REQUIRES_SUPER; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /FishEyeDemo/FishEyeDemo-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | net.macoscope.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /FishEyeDemo.xcodeproj/project.xcworkspace/xcshareddata/FishEyeDemo.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | B31253C6-69A2-4987-9052-C92B4E1A6BA8 9 | IDESourceControlProjectName 10 | FishEyeDemo 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | A5EB103F-4984-443B-A8A2-702CD44C6DB9 14 | ssh://github.com/macoscope/MCSFishEye.git 15 | 16 | IDESourceControlProjectPath 17 | FishEyeDemo.xcodeproj/project.xcworkspace 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | A5EB103F-4984-443B-A8A2-702CD44C6DB9 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | ssh://github.com/macoscope/MCSFishEye.git 25 | IDESourceControlProjectVersion 26 | 110 27 | IDESourceControlProjectWCCIdentifier 28 | A5EB103F-4984-443B-A8A2-702CD44C6DB9 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | A5EB103F-4984-443B-A8A2-702CD44C6DB9 36 | IDESourceControlWCCName 37 | MCSFishEye 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSDemoFishEyeItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCSDemoFishEyeItem.m 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 8/30/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import "MCSDemoFishEyeItem.h" 10 | #import 11 | 12 | @interface MCSDemoFishEyeItem() 13 | @end 14 | 15 | @implementation MCSDemoFishEyeItem 16 | 17 | - (id)initWithFrame:(CGRect)frame 18 | { 19 | self = [super initWithFrame:frame]; 20 | if (self) { 21 | _backgroundView = [[UIView alloc] initWithFrame:self.bounds]; 22 | _backgroundView.layer.cornerRadius = 24.0f; 23 | 24 | _label = [[UILabel alloc] initWithFrame:self.bounds]; 25 | _label.textAlignment = NSTextAlignmentCenter; 26 | _label.font = [UIFont systemFontOfSize:40.0f]; 27 | _label.backgroundColor = [UIColor clearColor]; 28 | 29 | [self addSubview:_backgroundView]; 30 | [self addSubview:_label]; 31 | } 32 | return self; 33 | } 34 | 35 | - (void)layoutSubviews 36 | { 37 | [super layoutSubviews]; 38 | _backgroundView.frame = CGRectInset(self.bounds, 5.0, 5.0); 39 | _label.frame = CGRectInset(self.bounds, 5.0, 5.0); 40 | 41 | } 42 | 43 | - (UIColor *)selectedColor 44 | { 45 | return [UIColor colorWithRed:192.0f/255.0f green:41.0f/255.0f blue:66.0f/255.0f alpha:1.0]; 46 | } 47 | 48 | - (UIColor *)highlightedColor 49 | { 50 | return [UIColor colorWithRed:217.0f/255.0f green:91.0f/255.0f blue:67.0f/255.0f alpha:1.0]; 51 | } 52 | 53 | - (UIColor *)defaultColor 54 | { 55 | return [UIColor colorWithRed:236.0f/255.0f green:208.0f/255.0f blue:120.0f/255.0f alpha:1.0]; 56 | } 57 | 58 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated 59 | { 60 | [super setSelected:selected animated:animated]; 61 | 62 | [UIView animateWithDuration:animated ? 0.2 : 0.0 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ 63 | if (selected) { 64 | self.backgroundView.backgroundColor = [self selectedColor]; 65 | self.label.textColor = [UIColor colorWithWhite:1.0 alpha:1.0]; 66 | } else { 67 | self.backgroundView.backgroundColor = [self defaultColor]; 68 | self.label.textColor = [UIColor colorWithWhite:0.2 alpha:0.7]; 69 | } 70 | } completion:NULL]; 71 | } 72 | 73 | - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated 74 | { 75 | [super setHighlighted:highlighted animated:animated]; 76 | 77 | [UIView animateWithDuration:animated ? 0.2 : 0.0 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ 78 | if (highlighted) { 79 | self.backgroundView.backgroundColor = [self highlightedColor]; 80 | self.label.textColor = [UIColor colorWithWhite:1.0 alpha:1.0]; 81 | } else { 82 | self.backgroundView.backgroundColor = [self defaultColor]; 83 | self.label.textColor = [UIColor colorWithWhite:0.2 alpha:0.7]; 84 | } 85 | } completion:NULL]; 86 | 87 | } 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCSViewController.m 3 | // FishEyeDemo 4 | // 5 | // Created by Bartosz Ciechanowski on 8/30/13. 6 | // Copyright (c) 2013 Macoscope. All rights reserved. 7 | // 8 | 9 | #import "MCSViewController.h" 10 | #import "MCSFishEyeView.h" 11 | #import "MCSDemoFishEyeItem.h" 12 | 13 | @interface MCSViewController () 14 | 15 | @property (weak, nonatomic) IBOutlet MCSFishEyeView *leftFishEyeView; 16 | @property (weak, nonatomic) IBOutlet MCSFishEyeView *topFishEyeView; 17 | @property (weak, nonatomic) IBOutlet MCSFishEyeView *rightFishEyeView; 18 | @property (weak, nonatomic) IBOutlet MCSFishEyeView *bottomFishEyeView; 19 | 20 | @property (strong, nonatomic) IBOutletCollection(MCSFishEyeView) NSArray *fishEyeViews; 21 | 22 | @end 23 | 24 | @implementation MCSViewController 25 | 26 | - (void)viewDidLoad 27 | { 28 | [super viewDidLoad]; 29 | 30 | self.leftFishEyeView.itemSize = CGSizeMake(120.0, 120.0); 31 | self.leftFishEyeView.contentInset = UIEdgeInsetsMake(20.0, 5.0, 20.0, 0.0); 32 | [self.leftFishEyeView registerItemClass:[MCSDemoFishEyeItem class]]; 33 | 34 | self.rightFishEyeView.itemSize = CGSizeMake(130.0, 130.0); 35 | self.rightFishEyeView.contentInset = UIEdgeInsetsMake(20.0, 0.0, 20.0, 10.0); 36 | self.rightFishEyeView.expansionDirection = MCSFishEyeExpansionDirectionLeft; 37 | self.rightFishEyeView.selectedItemOffset = 80.0f; 38 | [self.rightFishEyeView registerItemNib:[UINib nibWithNibName:@"MCSDemoXibFishEyeViewItem" bundle:nil]]; 39 | 40 | self.topFishEyeView.itemSize = CGSizeMake(70.0, 70.0); 41 | self.topFishEyeView.expansionDirection = MCSFishEyeExpansionDirectionBottom; 42 | self.topFishEyeView.contentInset = UIEdgeInsetsMake(14.0, 0.0, 0.0, 0.0); 43 | [self.topFishEyeView registerItemClass:[MCSDemoFishEyeItem class]]; 44 | 45 | self.bottomFishEyeView.itemSize = CGSizeMake(90.0, 90.0); 46 | self.bottomFishEyeView.contentInset = UIEdgeInsetsMake(0.0, 40.0, 0.0, 40.0); 47 | self.bottomFishEyeView.expansionDirection = MCSFishEyeExpansionDirectionTop; 48 | self.bottomFishEyeView.selectedItemOffset = 40.0f; 49 | [self.bottomFishEyeView registerItemClass:[MCSDemoFishEyeItem class]]; 50 | 51 | for (MCSFishEyeView *fishEye in self.fishEyeViews) { 52 | fishEye.dataSource = self; 53 | fishEye.delegate = self; 54 | 55 | 56 | [fishEye reloadData]; 57 | } 58 | } 59 | 60 | #pragma mark - FishEye Data Source 61 | 62 | - (NSUInteger)numberOfItemsInFishEyeView:(MCSFishEyeView *)fishEyeView 63 | { 64 | return fishEyeView == self.rightFishEyeView ? 4 : 20; 65 | } 66 | 67 | - (void)fishEyeView:(MCSFishEyeView *)fishEyeView configureItem:(MCSDemoFishEyeItem *)item atIndex:(NSUInteger)index 68 | { 69 | if (fishEyeView == self.leftFishEyeView) { 70 | item.label.text = [@(index + 1) stringValue]; 71 | } else { 72 | item.label.text = [NSString stringWithFormat:@"%c", 'A' + index]; 73 | } 74 | } 75 | 76 | #pragma mark - FishEye Delegate 77 | 78 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 79 | { 80 | for (MCSFishEyeView *fishEye in self.fishEyeViews) { 81 | [fishEye deselectSelectedItemAnimated:YES]; 82 | } 83 | } 84 | 85 | @end 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MCSFishEyeView 2 | ========== 3 | 4 | The fisheye from our [Bubble Browser](http://bubblebrowserapp.com) for iPad. 5 | 6 | [![](https://raw.github.com/macoscope/MCSFisheye/master/Screens/fishEye.gif)](https://raw.github.com/macoscope/MCSFisheye/master/Screens/fishEye.gif) 7 | 8 | 9 | ## How To Use 10 | 11 | Checkout demo project for some real life action! 12 | 13 | ### Instantiating 14 | 15 | Setup a `dataSource`, an optional `delegate`, add subview and you're done 16 | 17 | ``` 18 | MCSFishEyeView *fisheye = [[MCSFishEyeView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 50.0f, 600.0f)]; 19 | fisheye.dataSource = self; 20 | fisheye.delegate = self; 21 | [self.view addSubview:fisheye]; 22 | ``` 23 | 24 | ### Items 25 | 26 | `MCSFishEyeView` has the notion of items, which are more or less similar to table view cells. Each item should be a subclass of `MCSFishEyeViewItem`, as it already provides convinent methods for setting `highlighted` and `selected` state. You register a class to `MCSFishEyeView` by this one-liner: 27 | 28 | ``` 29 | [fisheye registerItemClass:[MCSExampleFishEyeItem class]]; 30 | ``` 31 | Inside your custom `MCSFishEyeViewItem` subclass you are free to do whatever you want in both 32 | 33 | `- (void)setSelected:(BOOL)selected animated:(BOOL)animated` 34 | 35 | and 36 | 37 | `- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated` 38 | 39 | methods, just make sure you call `super` (don't worry, compiler will warn you if you forget to do so). 40 | 41 | ### States 42 | 43 | `MCSFishEyeView` has three different states: 44 | 45 | 46 | 47 | - `MCSFishEyeStateCollapsed` - in this state all the items are collapsed, no item is `highlighted` or `selected` 48 | 49 | [![](https://raw.github.com/macoscope/MCSFisheye/master/Screens/collapsed.png)](https://raw.github.com/macoscope/MCSFisheye/master/Screens/collapsed.png) 50 | 51 | - `MCSFishEyeStateExpandedActive` - items are moving around according to touch location, at most one element is `highlighted`, no elements are `selected` 52 | 53 | [![](https://raw.github.com/macoscope/MCSFisheye/master/Screens/highlighted.png)](https://raw.github.com/macoscope/MCSFisheye/master/Screens/highlighted.png) 54 | 55 | - `MCSFishEyeStateExpandedPassive` - in this state a single item is standing out in `selected` state, all the other items are collapsed 56 | 57 | [![](https://raw.github.com/macoscope/MCSFisheye/master/Screens/selected.png)](https://raw.github.com/macoscope/MCSFisheye/master/Screens/selected.png) 58 | 59 | 60 | ## Detailed explanation 61 | 62 | For more detailed description of how `MCSFishEye` works checkout [this post on our blog](http://macoscope.com/blog/bubble-browsers-fisheye/)! 63 | 64 | 65 | ## Requirements 66 | 67 | - iOS 5.0 68 | - ARC 69 | - QuartzCore framework in your project 70 | 71 | 72 | ## Contact 73 | 74 | [Macoscope](http://macoscope.com) 75 | 76 | ## License 77 | 78 | Copyright 2013 Macoscope, sp z o.o. 79 | 80 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 81 | this file except in compliance with the License. You may obtain a copy of the 82 | License at 83 | 84 | http://www.apache.org/licenses/LICENSE-2.0 85 | 86 | Unless required by applicable law or agreed to in writing, software distributed 87 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 88 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 89 | specific language governing permissions and limitations under the License. 90 | 91 | -------------------------------------------------------------------------------- /MCSFishEyeView/MCSFishEyeView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MCSFishEyeView.h 3 | // 4 | // Created by Bartosz Ciechanowski on 2/25/13. 5 | // Copyright (c) 2013 Macoscope. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @class MCSFishEyeView, MCSFishEyeViewItem; 11 | 12 | typedef NS_ENUM(NSInteger, MCSFishEyeState) { 13 | MCSFishEyeStateCollapsed, // all elements are docked in 14 | MCSFishEyeStateExpandedActive, // touch event happening, elements are moving around 15 | MCSFishEyeStateExpandedPassive // single element is out, rest is docked in 16 | }; 17 | 18 | typedef NS_ENUM(NSInteger, MCSFishEyeExpansionDirection) { 19 | MCSFishEyeExpansionDirectionRight, 20 | MCSFishEyeExpansionDirectionLeft, 21 | MCSFishEyeExpansionDirectionTop, 22 | MCSFishEyeExpansionDirectionBottom 23 | }; 24 | 25 | // those methods get called only after reloadData 26 | @protocol MCSFishEyeViewDataSource 27 | 28 | - (NSUInteger)numberOfItemsInFishEyeView:(MCSFishEyeView *)fishEyeView; 29 | - (void)fishEyeView:(MCSFishEyeView *)fishEyeView configureItem:(MCSFishEyeViewItem *)item atIndex:(NSUInteger)index; 30 | 31 | @end 32 | 33 | @protocol MCSFishEyeViewDelegate 34 | 35 | @optional 36 | - (void)fishEyeView:(MCSFishEyeView *)fishEyeView willChangeToState:(MCSFishEyeState)newState; 37 | - (void)fishEyeView:(MCSFishEyeView *)fishEyeView didChangeFromState:(MCSFishEyeState)oldState; 38 | 39 | - (BOOL)fishEyeView:(MCSFishEyeView *)fishEyeView shouldHighlightItemAtIndex:(NSUInteger)index; 40 | - (void)fishEyeView:(MCSFishEyeView *)fishEyeView didHighlightItemAtIndex:(NSUInteger)index; 41 | - (void)fishEyeView:(MCSFishEyeView *)fishEyeView didUnhighlightItemAtIndex:(NSUInteger)index; 42 | 43 | - (BOOL)fishEyeView:(MCSFishEyeView *)fishEyeView shouldSelectItemAtIndex:(NSUInteger)index; 44 | - (void)fishEyeView:(MCSFishEyeView *)fishEyeView didSelectItemAtIndex:(NSUInteger)index; 45 | - (void)fishEyeView:(MCSFishEyeView *)fishEyeView didDeselectItemAtIndex:(NSUInteger)index; 46 | 47 | @end 48 | 49 | @interface MCSFishEyeView : UIView 50 | 51 | @property (nonatomic, weak) id dataSource; 52 | @property (nonatomic, weak) id delegate; 53 | 54 | @property (nonatomic, readonly) MCSFishEyeState state; 55 | 56 | @property (nonatomic) MCSFishEyeExpansionDirection expansionDirection; // defaults to MCSFishEyeExpansionDirectionRight 57 | @property (nonatomic) BOOL evadesFinger; // if YES, then fisheye's items will get translated in expansion direction, so that they're not overlapped by finger, defaults to YES 58 | 59 | @property (nonatomic) CGSize itemSize; // size of fully expanded item, defaults to CGSizeMake(100.0f, 100.0f); 60 | @property (nonatomic) CGFloat selectedItemOffset; // offset of selected item in the direction of expansion, defaults to 50.0f 61 | @property (nonatomic) UIEdgeInsets contentInset; // additional insets applied when layouting items, defaults to UIEdgeInsetsZero 62 | 63 | @property (nonatomic, readonly) NSUInteger highlightedIndex; // returns NSNotFound if none is highlighted 64 | @property (nonatomic, readonly) NSUInteger selectedIndex; // returns NSNotFound if none is selected 65 | 66 | // the last one used counts 67 | - (void)registerItemNib:(UINib *)nib; // the nib file must contain only one top-level object and that object must be subclass of MCSFishEyeViewItem 68 | - (void)registerItemClass:(Class)itemClass; // itemClass must be subclass of MCSFishEyeViewItem 69 | 70 | - (void)reloadData; 71 | 72 | - (void)selectItemAtIndex:(NSUInteger)index animated:(BOOL)animated; // will not notify delegate 73 | - (void)deselectSelectedItemAnimated:(BOOL)animated; // will not notify delegate 74 | 75 | - (MCSFishEyeViewItem *)itemAtIndex:(NSUInteger)index; 76 | - (NSUInteger)indexForItem:(MCSFishEyeViewItem *)item; 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSDemoXibFishEyeViewItem.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1280 5 | 12E55 6 | 4510 7 | 1187.39 8 | 626.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 3742 12 | 13 | 14 | IBProxyObject 15 | IBUILabel 16 | IBUIView 17 | 18 | 19 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 20 | 21 | 22 | PluginDependencyRecalculationVersion 23 | 24 | 25 | 26 | 27 | IBFilesOwner 28 | IBIPadFramework 29 | 30 | 31 | IBFirstResponder 32 | IBIPadFramework 33 | 34 | 35 | 36 | 1298 37 | 38 | 39 | 40 | 1298 41 | {{5, 5}, {120, 120}} 42 | 43 | 44 | 45 | _NS:9 46 | 47 | 1 48 | MC4xNTE5NTE5NTM5IDAuMTkzNTUyMjU4MiAwLjI5Nzk1MTIxMTcAA 49 | 50 | IBIPadFramework 51 | 52 | 53 | 54 | 1290 55 | {{0, 41}, {130, 48}} 56 | 57 | 58 | _NS:9 59 | {251, 251} 60 | NO 61 | YES 62 | 7 63 | NO 64 | IBIPadFramework 65 | 1 66 | 67 | 1 68 | MSAxIDEAA 69 | 70 | 71 | 0 72 | 1 73 | 74 | HelveticaNeue-Light 75 | Helvetica Neue 76 | 0 77 | 44 78 | 79 | 80 | HelveticaNeue-Light 81 | 44 82 | 16 83 | 84 | NO 85 | 86 | 87 | 88 | {130, 130} 89 | 90 | 91 | 92 | 93 | 3 94 | MCAwAA 95 | 96 | 97 | IBUISimulatedFreeformSizeMetricsSentinel 98 | Freeform 99 | 100 | IBIPadFramework 101 | 102 | 103 | 104 | NO 105 | 106 | 107 | 108 | label 109 | 110 | 111 | 112 | KBP-Lm-miJ 113 | 114 | 115 | 116 | 117 | 118 | 0 119 | 120 | 121 | 122 | 123 | 124 | -1 125 | 126 | 127 | File's Owner 128 | 129 | 130 | -2 131 | 132 | 133 | 134 | 135 | 1 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | FPT-DN-MrP 145 | 146 | 147 | 148 | 149 | YzM-dm-kNv 150 | 151 | 152 | 153 | 154 | 155 | 156 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 157 | 158 | UIResponder 159 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 160 | 161 | MCSDemoXibFishEyeViewItem 162 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 163 | 164 | 165 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 166 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | MCSDemoXibFishEyeViewItem 177 | MCSFishEyeViewItem 178 | 179 | label 180 | UILabel 181 | 182 | 183 | label 184 | 185 | label 186 | UILabel 187 | 188 | 189 | 190 | IBProjectSource 191 | ./Classes/MCSDemoXibFishEyeViewItem.h 192 | 193 | 194 | 195 | MCSFishEyeViewItem 196 | UIView 197 | 198 | IBProjectSource 199 | ./Classes/MCSFishEyeViewItem.h 200 | 201 | 202 | 203 | 204 | 0 205 | IBIPadFramework 206 | YES 207 | 208 | com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS 209 | 210 | 211 | 212 | com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 213 | 214 | 215 | YES 216 | 3 217 | 3742 218 | 219 | 220 | -------------------------------------------------------------------------------- /FishEyeDemo/MCSViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1280 5 | 12E55 6 | 4488.1 7 | 1187.39 8 | 626.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 3715.3 12 | 13 | 14 | IBProxyObject 15 | IBUIView 16 | 17 | 18 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 19 | 20 | 21 | PluginDependencyRecalculationVersion 22 | 23 | 24 | 25 | 26 | IBFilesOwner 27 | IBIPadFramework 28 | 29 | 30 | IBFirstResponder 31 | IBIPadFramework 32 | 33 | 34 | 35 | 1316 36 | 37 | 38 | 39 | 1300 40 | 41 | {60, 894} 42 | 43 | 44 | 45 | 3 46 | MCAwAA 47 | 48 | IBIPadFramework 49 | 50 | 51 | 52 | 1321 53 | 54 | {{657, 352}, {111, 300}} 55 | 56 | 57 | 58 | IBIPadFramework 59 | 60 | 61 | 62 | 1290 63 | 64 | {{0, 909}, {768, 95}} 65 | 66 | 67 | 68 | IBIPadFramework 69 | 70 | 71 | 72 | 1314 73 | 74 | {{86, 0}, {597, 54}} 75 | 76 | 77 | 78 | IBIPadFramework 79 | 80 | 81 | 82 | {{0, 20}, {768, 1004}} 83 | 84 | 85 | 86 | 3 87 | MQA 88 | 89 | 2 90 | 91 | 92 | NO 93 | 94 | 2 95 | 96 | IBIPadFramework 97 | 98 | 99 | 100 | NO 101 | 102 | 103 | 104 | bottomFishEyeView 105 | 106 | 107 | 108 | X5M-Ft-47q 109 | 110 | 111 | 112 | leftFishEyeView 113 | 114 | 115 | 116 | yPg-n2-TN2 117 | 118 | 119 | 120 | rightFishEyeView 121 | 122 | 123 | 124 | yCR-c6-eyV 125 | 126 | 127 | 128 | topFishEyeView 129 | 130 | 131 | 132 | 4Ci-iz-yOr 133 | 134 | 135 | 136 | view 137 | 138 | 139 | 140 | 3 141 | 142 | 143 | 144 | fishEyeViews 145 | 146 | 147 | NSArray 148 | NO 149 | 150 | Tzv-8e-SY4 151 | 152 | 153 | 154 | fishEyeViews 155 | 156 | 157 | NSArray 158 | NO 159 | 160 | 6AJ-Cc-2qb 161 | 162 | 163 | 164 | fishEyeViews 165 | 166 | 167 | NSArray 168 | NO 169 | 170 | O4X-U5-iIL 171 | 172 | 173 | 174 | fishEyeViews 175 | 176 | 177 | NSArray 178 | NO 179 | 180 | fGE-0y-LA7 181 | 182 | 183 | 184 | 185 | 186 | 0 187 | 188 | 189 | 190 | 191 | 192 | -1 193 | 194 | 195 | File's Owner 196 | 197 | 198 | -2 199 | 200 | 201 | 202 | 203 | 2 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | dOc-Kq-xUJ 215 | 216 | 217 | 218 | 219 | koS-6J-5J5 220 | 221 | 222 | 223 | 224 | Vok-Ng-ZoO 225 | 226 | 227 | 228 | 229 | sGD-WF-cgg 230 | 231 | 232 | 233 | 234 | 235 | 236 | MCSViewController 237 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 238 | 239 | UIResponder 240 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 241 | 242 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 243 | 244 | 245 | MCSFishEyeView 246 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 247 | 248 | 249 | MCSFishEyeView 250 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 251 | 252 | 253 | MCSFishEyeView 254 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 255 | 256 | 257 | MCSFishEyeView 258 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 0 269 | IBIPadFramework 270 | YES 271 | 272 | com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS 273 | 274 | 275 | 276 | com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 277 | 278 | 279 | YES 280 | 3 281 | 3715.3 282 | 283 | 284 | -------------------------------------------------------------------------------- /FishEyeDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 296F083D17CF8FA000D1906F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296F083C17CF8FA000D1906F /* Foundation.framework */; }; 11 | 296F083F17CF8FA000D1906F /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296F083E17CF8FA000D1906F /* CoreGraphics.framework */; }; 12 | 296F084117CF8FA000D1906F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296F084017CF8FA000D1906F /* UIKit.framework */; }; 13 | 296F084717CF8FA000D1906F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 296F084517CF8FA000D1906F /* InfoPlist.strings */; }; 14 | 296F084917CF8FA000D1906F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 296F084817CF8FA000D1906F /* main.m */; }; 15 | 296F084D17CF8FA000D1906F /* MCSAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 296F084C17CF8FA000D1906F /* MCSAppDelegate.m */; }; 16 | 29B71BB117D09B5F00AA7946 /* MCSViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B71BAF17D09B5F00AA7946 /* MCSViewController.m */; }; 17 | 29B71BB217D09B5F00AA7946 /* MCSViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B71BB017D09B5F00AA7946 /* MCSViewController.xib */; }; 18 | 29B71BFB17D0A63C00AA7946 /* MCSDemoFishEyeItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B71BFA17D0A63C00AA7946 /* MCSDemoFishEyeItem.m */; }; 19 | 29B71BFD17D0D4A000AA7946 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B71BFC17D0D4A000AA7946 /* QuartzCore.framework */; }; 20 | 29E2EDEF17D61E0200BEEA47 /* MCSFishEyeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E2EDEC17D61E0200BEEA47 /* MCSFishEyeView.m */; }; 21 | 29E2EDF017D61E0200BEEA47 /* MCSFishEyeViewItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E2EDEE17D61E0200BEEA47 /* MCSFishEyeViewItem.m */; }; 22 | 29E2EDF317D6207F00BEEA47 /* MCSDemoXibFishEyeViewItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E2EDF217D6207F00BEEA47 /* MCSDemoXibFishEyeViewItem.m */; }; 23 | 29E2EDF517D6220E00BEEA47 /* MCSDemoXibFishEyeViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29E2EDF417D6209C00BEEA47 /* MCSDemoXibFishEyeViewItem.xib */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 296F083917CF8FA000D1906F /* FishEyeDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FishEyeDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 296F083C17CF8FA000D1906F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 29 | 296F083E17CF8FA000D1906F /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 30 | 296F084017CF8FA000D1906F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 31 | 296F084417CF8FA000D1906F /* FishEyeDemo-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FishEyeDemo-Info.plist"; sourceTree = ""; }; 32 | 296F084617CF8FA000D1906F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 33 | 296F084817CF8FA000D1906F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 34 | 296F084A17CF8FA000D1906F /* FishEyeDemo-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FishEyeDemo-Prefix.pch"; sourceTree = ""; }; 35 | 296F084B17CF8FA000D1906F /* MCSAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCSAppDelegate.h; sourceTree = ""; }; 36 | 296F084C17CF8FA000D1906F /* MCSAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MCSAppDelegate.m; sourceTree = ""; }; 37 | 29B71BAE17D09B5F00AA7946 /* MCSViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCSViewController.h; sourceTree = ""; }; 38 | 29B71BAF17D09B5F00AA7946 /* MCSViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = MCSViewController.m; sourceTree = ""; }; 39 | 29B71BB017D09B5F00AA7946 /* MCSViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MCSViewController.xib; sourceTree = ""; }; 40 | 29B71BF917D0A63C00AA7946 /* MCSDemoFishEyeItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCSDemoFishEyeItem.h; sourceTree = ""; }; 41 | 29B71BFA17D0A63C00AA7946 /* MCSDemoFishEyeItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCSDemoFishEyeItem.m; sourceTree = ""; }; 42 | 29B71BFC17D0D4A000AA7946 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 43 | 29E2EDEB17D61E0200BEEA47 /* MCSFishEyeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCSFishEyeView.h; sourceTree = ""; }; 44 | 29E2EDEC17D61E0200BEEA47 /* MCSFishEyeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCSFishEyeView.m; sourceTree = ""; }; 45 | 29E2EDED17D61E0200BEEA47 /* MCSFishEyeViewItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCSFishEyeViewItem.h; sourceTree = ""; }; 46 | 29E2EDEE17D61E0200BEEA47 /* MCSFishEyeViewItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCSFishEyeViewItem.m; sourceTree = ""; }; 47 | 29E2EDF117D6207F00BEEA47 /* MCSDemoXibFishEyeViewItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MCSDemoXibFishEyeViewItem.h; sourceTree = ""; }; 48 | 29E2EDF217D6207F00BEEA47 /* MCSDemoXibFishEyeViewItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MCSDemoXibFishEyeViewItem.m; sourceTree = ""; }; 49 | 29E2EDF417D6209C00BEEA47 /* MCSDemoXibFishEyeViewItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MCSDemoXibFishEyeViewItem.xib; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 296F083617CF8FA000D1906F /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | 29B71BFD17D0D4A000AA7946 /* QuartzCore.framework in Frameworks */, 58 | 296F083F17CF8FA000D1906F /* CoreGraphics.framework in Frameworks */, 59 | 296F084117CF8FA000D1906F /* UIKit.framework in Frameworks */, 60 | 296F083D17CF8FA000D1906F /* Foundation.framework in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 296F083017CF8FA000D1906F = { 68 | isa = PBXGroup; 69 | children = ( 70 | 29E2EDEA17D61E0200BEEA47 /* MCSFishEyeView */, 71 | 296F084217CF8FA000D1906F /* FishEyeDemo */, 72 | 296F083B17CF8FA000D1906F /* Frameworks */, 73 | 296F083A17CF8FA000D1906F /* Products */, 74 | ); 75 | indentWidth = 2; 76 | sourceTree = ""; 77 | tabWidth = 2; 78 | }; 79 | 296F083A17CF8FA000D1906F /* Products */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 296F083917CF8FA000D1906F /* FishEyeDemo.app */, 83 | ); 84 | name = Products; 85 | sourceTree = ""; 86 | }; 87 | 296F083B17CF8FA000D1906F /* Frameworks */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 29B71BFC17D0D4A000AA7946 /* QuartzCore.framework */, 91 | 296F083C17CF8FA000D1906F /* Foundation.framework */, 92 | 296F083E17CF8FA000D1906F /* CoreGraphics.framework */, 93 | 296F084017CF8FA000D1906F /* UIKit.framework */, 94 | ); 95 | name = Frameworks; 96 | sourceTree = ""; 97 | }; 98 | 296F084217CF8FA000D1906F /* FishEyeDemo */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 296F084B17CF8FA000D1906F /* MCSAppDelegate.h */, 102 | 296F084C17CF8FA000D1906F /* MCSAppDelegate.m */, 103 | 29B71BF917D0A63C00AA7946 /* MCSDemoFishEyeItem.h */, 104 | 29B71BFA17D0A63C00AA7946 /* MCSDemoFishEyeItem.m */, 105 | 29E2EDF117D6207F00BEEA47 /* MCSDemoXibFishEyeViewItem.h */, 106 | 29E2EDF217D6207F00BEEA47 /* MCSDemoXibFishEyeViewItem.m */, 107 | 29E2EDF417D6209C00BEEA47 /* MCSDemoXibFishEyeViewItem.xib */, 108 | 29B71BAE17D09B5F00AA7946 /* MCSViewController.h */, 109 | 29B71BAF17D09B5F00AA7946 /* MCSViewController.m */, 110 | 29B71BB017D09B5F00AA7946 /* MCSViewController.xib */, 111 | 296F084317CF8FA000D1906F /* Supporting Files */, 112 | ); 113 | path = FishEyeDemo; 114 | sourceTree = ""; 115 | }; 116 | 296F084317CF8FA000D1906F /* Supporting Files */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 296F084417CF8FA000D1906F /* FishEyeDemo-Info.plist */, 120 | 296F084517CF8FA000D1906F /* InfoPlist.strings */, 121 | 296F084817CF8FA000D1906F /* main.m */, 122 | 296F084A17CF8FA000D1906F /* FishEyeDemo-Prefix.pch */, 123 | ); 124 | name = "Supporting Files"; 125 | sourceTree = ""; 126 | }; 127 | 29E2EDEA17D61E0200BEEA47 /* MCSFishEyeView */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 29E2EDEB17D61E0200BEEA47 /* MCSFishEyeView.h */, 131 | 29E2EDEC17D61E0200BEEA47 /* MCSFishEyeView.m */, 132 | 29E2EDED17D61E0200BEEA47 /* MCSFishEyeViewItem.h */, 133 | 29E2EDEE17D61E0200BEEA47 /* MCSFishEyeViewItem.m */, 134 | ); 135 | path = MCSFishEyeView; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXNativeTarget section */ 141 | 296F083817CF8FA000D1906F /* FishEyeDemo */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = 296F086E17CF8FA000D1906F /* Build configuration list for PBXNativeTarget "FishEyeDemo" */; 144 | buildPhases = ( 145 | 296F083517CF8FA000D1906F /* Sources */, 146 | 296F083617CF8FA000D1906F /* Frameworks */, 147 | 296F083717CF8FA000D1906F /* Resources */, 148 | ); 149 | buildRules = ( 150 | ); 151 | dependencies = ( 152 | ); 153 | name = FishEyeDemo; 154 | productName = FishEyeDemo; 155 | productReference = 296F083917CF8FA000D1906F /* FishEyeDemo.app */; 156 | productType = "com.apple.product-type.application"; 157 | }; 158 | /* End PBXNativeTarget section */ 159 | 160 | /* Begin PBXProject section */ 161 | 296F083117CF8FA000D1906F /* Project object */ = { 162 | isa = PBXProject; 163 | attributes = { 164 | CLASSPREFIX = MCS; 165 | LastUpgradeCheck = 0500; 166 | ORGANIZATIONNAME = Macoscope; 167 | }; 168 | buildConfigurationList = 296F083417CF8FA000D1906F /* Build configuration list for PBXProject "FishEyeDemo" */; 169 | compatibilityVersion = "Xcode 3.2"; 170 | developmentRegion = English; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | Base, 175 | ); 176 | mainGroup = 296F083017CF8FA000D1906F; 177 | productRefGroup = 296F083A17CF8FA000D1906F /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 296F083817CF8FA000D1906F /* FishEyeDemo */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 296F083717CF8FA000D1906F /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 29B71BB217D09B5F00AA7946 /* MCSViewController.xib in Resources */, 192 | 29E2EDF517D6220E00BEEA47 /* MCSDemoXibFishEyeViewItem.xib in Resources */, 193 | 296F084717CF8FA000D1906F /* InfoPlist.strings in Resources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | /* End PBXResourcesBuildPhase section */ 198 | 199 | /* Begin PBXSourcesBuildPhase section */ 200 | 296F083517CF8FA000D1906F /* Sources */ = { 201 | isa = PBXSourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | 29E2EDF317D6207F00BEEA47 /* MCSDemoXibFishEyeViewItem.m in Sources */, 205 | 29E2EDEF17D61E0200BEEA47 /* MCSFishEyeView.m in Sources */, 206 | 29B71BB117D09B5F00AA7946 /* MCSViewController.m in Sources */, 207 | 296F084D17CF8FA000D1906F /* MCSAppDelegate.m in Sources */, 208 | 29E2EDF017D61E0200BEEA47 /* MCSFishEyeViewItem.m in Sources */, 209 | 296F084917CF8FA000D1906F /* main.m in Sources */, 210 | 29B71BFB17D0A63C00AA7946 /* MCSDemoFishEyeItem.m in Sources */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | /* End PBXSourcesBuildPhase section */ 215 | 216 | /* Begin PBXVariantGroup section */ 217 | 296F084517CF8FA000D1906F /* InfoPlist.strings */ = { 218 | isa = PBXVariantGroup; 219 | children = ( 220 | 296F084617CF8FA000D1906F /* en */, 221 | ); 222 | name = InfoPlist.strings; 223 | sourceTree = ""; 224 | }; 225 | /* End PBXVariantGroup section */ 226 | 227 | /* Begin XCBuildConfiguration section */ 228 | 296F086C17CF8FA000D1906F /* Debug */ = { 229 | isa = XCBuildConfiguration; 230 | buildSettings = { 231 | ALWAYS_SEARCH_USER_PATHS = NO; 232 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 233 | CLANG_CXX_LIBRARY = "libc++"; 234 | CLANG_ENABLE_MODULES = YES; 235 | CLANG_ENABLE_OBJC_ARC = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_CONSTANT_CONVERSION = YES; 238 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INT_CONVERSION = YES; 242 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 245 | COPY_PHASE_STRIP = NO; 246 | GCC_C_LANGUAGE_STANDARD = gnu99; 247 | GCC_DYNAMIC_NO_PIC = NO; 248 | GCC_OPTIMIZATION_LEVEL = 0; 249 | GCC_PREPROCESSOR_DEFINITIONS = ( 250 | "DEBUG=1", 251 | "$(inherited)", 252 | ); 253 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 255 | GCC_WARN_UNDECLARED_SELECTOR = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 257 | GCC_WARN_UNUSED_FUNCTION = YES; 258 | GCC_WARN_UNUSED_VARIABLE = YES; 259 | IPHONEOS_DEPLOYMENT_TARGET = 5.0; 260 | ONLY_ACTIVE_ARCH = YES; 261 | SDKROOT = iphoneos; 262 | TARGETED_DEVICE_FAMILY = "1,2"; 263 | }; 264 | name = Debug; 265 | }; 266 | 296F086D17CF8FA000D1906F /* Release */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ALWAYS_SEARCH_USER_PATHS = NO; 270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 271 | CLANG_CXX_LIBRARY = "libc++"; 272 | CLANG_ENABLE_MODULES = YES; 273 | CLANG_ENABLE_OBJC_ARC = YES; 274 | CLANG_WARN_BOOL_CONVERSION = YES; 275 | CLANG_WARN_CONSTANT_CONVERSION = YES; 276 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 277 | CLANG_WARN_EMPTY_BODY = YES; 278 | CLANG_WARN_ENUM_CONVERSION = YES; 279 | CLANG_WARN_INT_CONVERSION = YES; 280 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 281 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 282 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 283 | COPY_PHASE_STRIP = YES; 284 | ENABLE_NS_ASSERTIONS = NO; 285 | GCC_C_LANGUAGE_STANDARD = gnu99; 286 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 287 | GCC_WARN_UNDECLARED_SELECTOR = YES; 288 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 289 | GCC_WARN_UNUSED_FUNCTION = YES; 290 | GCC_WARN_UNUSED_VARIABLE = YES; 291 | IPHONEOS_DEPLOYMENT_TARGET = 5.0; 292 | SDKROOT = iphoneos; 293 | TARGETED_DEVICE_FAMILY = "1,2"; 294 | VALIDATE_PRODUCT = YES; 295 | }; 296 | name = Release; 297 | }; 298 | 296F086F17CF8FA000D1906F /* Debug */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 302 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 303 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 304 | GCC_PREFIX_HEADER = "FishEyeDemo/FishEyeDemo-Prefix.pch"; 305 | INFOPLIST_FILE = "FishEyeDemo/FishEyeDemo-Info.plist"; 306 | IPHONEOS_DEPLOYMENT_TARGET = 5.0; 307 | PRODUCT_NAME = "$(TARGET_NAME)"; 308 | WRAPPER_EXTENSION = app; 309 | }; 310 | name = Debug; 311 | }; 312 | 296F087017CF8FA000D1906F /* Release */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 316 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 317 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 318 | GCC_PREFIX_HEADER = "FishEyeDemo/FishEyeDemo-Prefix.pch"; 319 | INFOPLIST_FILE = "FishEyeDemo/FishEyeDemo-Info.plist"; 320 | IPHONEOS_DEPLOYMENT_TARGET = 5.0; 321 | PRODUCT_NAME = "$(TARGET_NAME)"; 322 | WRAPPER_EXTENSION = app; 323 | }; 324 | name = Release; 325 | }; 326 | /* End XCBuildConfiguration section */ 327 | 328 | /* Begin XCConfigurationList section */ 329 | 296F083417CF8FA000D1906F /* Build configuration list for PBXProject "FishEyeDemo" */ = { 330 | isa = XCConfigurationList; 331 | buildConfigurations = ( 332 | 296F086C17CF8FA000D1906F /* Debug */, 333 | 296F086D17CF8FA000D1906F /* Release */, 334 | ); 335 | defaultConfigurationIsVisible = 0; 336 | defaultConfigurationName = Release; 337 | }; 338 | 296F086E17CF8FA000D1906F /* Build configuration list for PBXNativeTarget "FishEyeDemo" */ = { 339 | isa = XCConfigurationList; 340 | buildConfigurations = ( 341 | 296F086F17CF8FA000D1906F /* Debug */, 342 | 296F087017CF8FA000D1906F /* Release */, 343 | ); 344 | defaultConfigurationIsVisible = 0; 345 | defaultConfigurationName = Release; 346 | }; 347 | /* End XCConfigurationList section */ 348 | }; 349 | rootObject = 296F083117CF8FA000D1906F /* Project object */; 350 | } 351 | -------------------------------------------------------------------------------- /MCSFishEyeView/MCSFishEyeView.m: -------------------------------------------------------------------------------- 1 | // 2 | // MCSFishEyeView.m 3 | // 4 | // Created by Bartosz Ciechanowski on 2/25/13. 5 | // Copyright (c) 2013 Macoscope. All rights reserved. 6 | // 7 | 8 | #import "MCSFishEyeView.h" 9 | #import "MCSFishEyeViewItem.h" 10 | 11 | #import 12 | 13 | /* 14 | If an item is within this distance to touch location, 15 | it will get gradually translated in the direction of expansion, 16 | so that this item is not obstructed by finger (only if 'evadesFinger' is set to YES) 17 | */ 18 | static const CGFloat FingerTranslationRadius = 100.0; 19 | 20 | /* 21 | Amount of translation applied to items in the direction of expansion 22 | (only if 'evadesFinger' is set to YES) 23 | */ 24 | static const CGFloat FingerTranslation = 60.0; 25 | 26 | /* 27 | Determines the maxium number of neighbors that get enalrged by expansion function 28 | */ 29 | static const NSInteger MaxExpansionFunctionNeighbors = 3; 30 | 31 | 32 | 33 | @interface MCSFishEyeView() 34 | { 35 | struct { 36 | unsigned int willChangeToState:1; 37 | unsigned int didChangeFromState:1; 38 | 39 | unsigned int shouldHighlight:1; 40 | unsigned int didHighlight:1; 41 | unsigned int didUnhighlight:1; 42 | 43 | unsigned int shouldSelect:1; 44 | unsigned int didSelect:1; 45 | unsigned int didDeselect:1; 46 | 47 | } _delegateRespondsTo; 48 | } 49 | 50 | @property (nonatomic, strong) NSArray *itemContainers; 51 | @property (nonatomic, strong) NSArray *items; 52 | 53 | @property (nonatomic, strong) UIView *transformView; 54 | 55 | @property (nonatomic, readwrite) MCSFishEyeState state; 56 | @property (nonatomic) NSInteger expansionsNeighbors; 57 | 58 | @property (nonatomic) CGAffineTransform itemTransform; 59 | @property (nonatomic) CGSize transformedItemSize; 60 | @property (nonatomic) CGFloat collapsedItemScale; 61 | @property (nonatomic) CGFloat collapsedItemHeight; 62 | 63 | @property (nonatomic) CGFloat startOffset; 64 | @property (nonatomic) CGFloat centeringOffset; 65 | 66 | @property (nonatomic, strong) Class itemClass; 67 | @property (nonatomic, strong) UINib *itemNib; 68 | 69 | @end 70 | 71 | @implementation MCSFishEyeView 72 | 73 | - (id)initWithFrame:(CGRect)frame 74 | { 75 | self = [super initWithFrame:frame]; 76 | if (self) { 77 | [self commonInit]; 78 | } 79 | return self; 80 | } 81 | 82 | - (id)initWithCoder:(NSCoder *)aDecoder 83 | { 84 | self = [super initWithCoder:aDecoder]; 85 | if (self) { 86 | [self commonInit]; 87 | } 88 | return self; 89 | } 90 | 91 | - (void)commonInit 92 | { 93 | _itemClass = [MCSFishEyeViewItem class]; 94 | _itemSize = CGSizeMake(100.0f, 100.0f); 95 | _selectedItemOffset = 50.0f; 96 | _contentInset = UIEdgeInsetsZero; 97 | _evadesFinger = YES; 98 | _expansionDirection = MCSFishEyeExpansionDirectionRight; 99 | 100 | _selectedIndex = NSNotFound; 101 | _highlightedIndex = NSNotFound; 102 | 103 | _transformView = [[UIView alloc] init]; 104 | _transformView.backgroundColor = [UIColor clearColor]; 105 | [self addSubview:_transformView]; 106 | } 107 | 108 | #pragma mark - Setters 109 | 110 | - (void)setDelegate:(id)delegate 111 | { 112 | _delegate = delegate; 113 | _delegateRespondsTo.willChangeToState = [delegate respondsToSelector:@selector(fishEyeView:willChangeToState:)]; 114 | _delegateRespondsTo.didChangeFromState = [delegate respondsToSelector:@selector(fishEyeView:didChangeFromState:)]; 115 | 116 | _delegateRespondsTo.shouldHighlight = [delegate respondsToSelector:@selector(fishEyeView:shouldHighlightItemAtIndex:)]; 117 | _delegateRespondsTo.didHighlight = [delegate respondsToSelector:@selector(fishEyeView:didHighlightItemAtIndex:)]; 118 | _delegateRespondsTo.didUnhighlight = [delegate respondsToSelector:@selector(fishEyeView:didUnhighlightItemAtIndex:)]; 119 | 120 | _delegateRespondsTo.shouldSelect = [delegate respondsToSelector:@selector(fishEyeView:shouldSelectItemAtIndex:)]; 121 | _delegateRespondsTo.didSelect = [delegate respondsToSelector:@selector(fishEyeView:didSelectItemAtIndex:)]; 122 | _delegateRespondsTo.didDeselect = [delegate respondsToSelector:@selector(fishEyeView:didDeselectItemAtIndex:)]; 123 | } 124 | 125 | - (void)setState:(MCSFishEyeState)newFishEyeState 126 | { 127 | if (_state == newFishEyeState) { 128 | return; 129 | } 130 | 131 | MCSFishEyeState oldFishEyeState = _state; 132 | 133 | if (_delegateRespondsTo.willChangeToState) { 134 | [self.delegate fishEyeView:self willChangeToState:newFishEyeState]; 135 | } 136 | 137 | _state = newFishEyeState; 138 | 139 | if (_delegateRespondsTo.didChangeFromState) { 140 | [self.delegate fishEyeView:self didChangeFromState:oldFishEyeState]; 141 | } 142 | } 143 | 144 | - (void)setItemSize:(CGSize)itemSize 145 | { 146 | _itemSize = itemSize; 147 | 148 | for (MCSFishEyeViewItem *item in self.itemContainers) { 149 | item.bounds = CGRectMake(0, 0, itemSize.width, itemSize.height); 150 | item.center = CGPointZero; 151 | } 152 | 153 | [self setNeedsLayout]; 154 | } 155 | 156 | - (void)setExpansionDirection:(MCSFishEyeExpansionDirection)expansionDirection 157 | { 158 | _expansionDirection = expansionDirection; 159 | [self setNeedsLayout]; 160 | } 161 | 162 | - (void)setContentInset:(UIEdgeInsets)contentInset 163 | { 164 | _contentInset = contentInset; 165 | [self setNeedsLayout]; 166 | } 167 | 168 | - (void)layoutSubviews 169 | { 170 | [super layoutSubviews]; 171 | 172 | [self repositionTransformView]; 173 | [self recalculateDimensions]; 174 | [self layoutWithCurrentState]; 175 | } 176 | 177 | - (void)setHighlightedIndex:(NSUInteger)highlightedIndex 178 | { 179 | if (highlightedIndex == _highlightedIndex) { 180 | return; 181 | } 182 | 183 | if (_highlightedIndex != NSNotFound) { 184 | [[self itemAtIndex:_highlightedIndex] setHighlighted:NO animated:YES]; 185 | if (_delegateRespondsTo.didUnhighlight) { 186 | [self.delegate fishEyeView:self didUnhighlightItemAtIndex:_highlightedIndex]; 187 | } 188 | } 189 | 190 | if (_delegateRespondsTo.shouldHighlight && ![self.delegate fishEyeView:self shouldHighlightItemAtIndex:highlightedIndex]) { 191 | highlightedIndex = NSNotFound; // don't highlight anything 192 | } 193 | 194 | _highlightedIndex = highlightedIndex; 195 | 196 | if (_highlightedIndex != NSNotFound) { 197 | [[self itemAtIndex:_highlightedIndex] setHighlighted:YES animated:YES]; 198 | if (_delegateRespondsTo.didHighlight) { 199 | [self.delegate fishEyeView:self didHighlightItemAtIndex:_highlightedIndex]; 200 | } 201 | } 202 | } 203 | 204 | - (void)setSelectedIndex:(NSUInteger)selectedIndex withDelegateCalls:(BOOL)shouldCallDelegate animated:(BOOL)animated 205 | { 206 | if (selectedIndex == _selectedIndex) { 207 | return; 208 | } 209 | 210 | if (_selectedIndex != NSNotFound) { 211 | [[self itemAtIndex:_selectedIndex] setSelected:NO animated:animated]; 212 | if (shouldCallDelegate && _delegateRespondsTo.didDeselect) { 213 | [self.delegate fishEyeView:self didDeselectItemAtIndex:_selectedIndex]; 214 | } 215 | } 216 | 217 | if (shouldCallDelegate && _delegateRespondsTo.shouldSelect && ![self.delegate fishEyeView:self shouldSelectItemAtIndex:selectedIndex]) { 218 | selectedIndex = NSNotFound; // don't select anything 219 | } 220 | 221 | _selectedIndex = selectedIndex; 222 | 223 | if (_selectedIndex != NSNotFound) { 224 | [self bringSubviewToFront:self.itemContainers[_selectedIndex]]; 225 | [[self itemAtIndex:_selectedIndex] setSelected:YES animated:animated]; 226 | if (shouldCallDelegate && _delegateRespondsTo.didSelect) { 227 | [self.delegate fishEyeView:self didSelectItemAtIndex:_selectedIndex]; 228 | } 229 | } 230 | } 231 | 232 | #pragma mark - Public functions 233 | 234 | - (void)registerItemClass:(Class)itemClass 235 | { 236 | NSParameterAssert(itemClass != nil); 237 | NSParameterAssert([itemClass isSubclassOfClass:[MCSFishEyeViewItem class]]); 238 | 239 | self.itemClass = itemClass; 240 | self.itemNib = nil; 241 | } 242 | 243 | - (void)registerItemNib:(UINib *)nib 244 | { 245 | NSParameterAssert(nib != nil); 246 | 247 | self.itemClass = nil; 248 | self.itemNib = nib; 249 | } 250 | 251 | - (void)reloadData 252 | { 253 | [self.items makeObjectsPerformSelector:@selector(removeFromSuperview)]; 254 | 255 | NSUInteger count = [self.dataSource numberOfItemsInFishEyeView:self]; 256 | NSMutableArray *items = [NSMutableArray arrayWithCapacity:count]; 257 | NSMutableArray *containers = [NSMutableArray arrayWithCapacity:count]; 258 | 259 | for (int i = 0; i < count; i++) { 260 | UIView *container = [[UIView alloc] init]; 261 | container.backgroundColor = [UIColor clearColor]; 262 | container.bounds = CGRectMake(0, 0, _itemSize.width, _itemSize.height); 263 | container.center = CGPointZero; 264 | 265 | MCSFishEyeViewItem *item; 266 | if (self.itemClass) { 267 | item = [[self.itemClass alloc] init]; 268 | } else if (self.itemNib) { 269 | NSArray *elements = [self.itemNib instantiateWithOwner:nil options:nil]; 270 | NSAssert(elements.count == 1, @"Instantiated NIB file doesn't have one top level object"); 271 | item = elements[0]; 272 | NSAssert([item isKindOfClass:[MCSFishEyeViewItem class]], @"Instantiated NIB object isn't subclass of MCSFishEyeViewItem"); 273 | } 274 | 275 | item.frame = container.bounds; 276 | item.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 277 | item.highlighted = NO; 278 | item.selected = NO; 279 | 280 | [self.dataSource fishEyeView:self configureItem:item atIndex:i]; 281 | 282 | [items addObject:item]; 283 | [containers addObject:container]; 284 | 285 | [container addSubview:item]; 286 | [self.transformView addSubview:container]; 287 | } 288 | 289 | self.items = items; 290 | self.itemContainers = containers; 291 | self.state = MCSFishEyeStateCollapsed; 292 | 293 | self.expansionsNeighbors = MIN(MaxExpansionFunctionNeighbors, items.count/2); 294 | 295 | [self recalculateDimensions]; 296 | [self layoutWithCurrentState]; 297 | [self collapseAnimated:NO notifying:NO]; 298 | } 299 | 300 | - (MCSFishEyeViewItem *)itemAtIndex:(NSUInteger)index 301 | { 302 | return self.items[index]; 303 | } 304 | 305 | - (NSUInteger)indexForItem:(MCSFishEyeViewItem *)item 306 | { 307 | return [self.items indexOfObject:item]; 308 | } 309 | 310 | - (void)selectItemAtIndex:(NSUInteger)index animated:(BOOL)animated 311 | { 312 | [self setState:MCSFishEyeStateExpandedPassive]; 313 | [self setSelectedIndex:index withDelegateCalls:NO animated:animated]; 314 | [self layoutItemsForOffset:[self offsetForIndex:index] withAnimationDuration:animated ? 0.2 : 0.0]; 315 | } 316 | 317 | - (void)deselectSelectedItemAnimated:(BOOL)animated 318 | { 319 | [self setSelectedIndex:NSNotFound withDelegateCalls:NO animated:YES]; 320 | [self collapseAnimated:animated notifying:NO]; 321 | } 322 | 323 | #pragma mark - Calculations 324 | 325 | - (void)repositionTransformView 326 | { 327 | CGRect bounds = self.bounds; 328 | bounds.origin.x += self.contentInset.left; 329 | bounds.origin.y += self.contentInset.top; 330 | bounds.size.width -= (self.contentInset.left + self.contentInset.right); 331 | bounds.size.height -= (self.contentInset.top + self.contentInset.bottom); 332 | 333 | CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); 334 | CGAffineTransform viewTransform, itemTransform; 335 | 336 | switch (self.expansionDirection) { 337 | case MCSFishEyeExpansionDirectionRight: 338 | viewTransform = CGAffineTransformIdentity; 339 | itemTransform = CGAffineTransformIdentity; 340 | break; 341 | case MCSFishEyeExpansionDirectionLeft: 342 | viewTransform = CGAffineTransformMakeScale(-1.0, 1.0); 343 | itemTransform = CGAffineTransformMakeScale(-1.0, 1.0); 344 | break; 345 | case MCSFishEyeExpansionDirectionTop: 346 | viewTransform = CGAffineTransformMakeRotation(-M_PI_2); 347 | itemTransform = CGAffineTransformMakeRotation(M_PI_2); 348 | bounds.size = CGSizeMake(bounds.size.height, bounds.size.width); 349 | break; 350 | case MCSFishEyeExpansionDirectionBottom: 351 | viewTransform = CGAffineTransformConcat(CGAffineTransformMakeRotation(M_PI_2), CGAffineTransformMakeScale(-1.0, 1.0)); 352 | itemTransform = CGAffineTransformConcat(CGAffineTransformMakeRotation(-M_PI_2), CGAffineTransformMakeScale(1.0, -1.0)); 353 | bounds.size = CGSizeMake(bounds.size.height, bounds.size.width); 354 | break; 355 | } 356 | 357 | bounds.origin = CGPointZero; 358 | 359 | self.transformView.bounds = bounds; 360 | self.transformView.center = center; 361 | self.transformView.transform = viewTransform; 362 | self.itemTransform = itemTransform; 363 | 364 | [self layoutWithCurrentState]; 365 | } 366 | 367 | - (void)recalculateDimensions 368 | { 369 | switch (self.expansionDirection) { 370 | case MCSFishEyeExpansionDirectionRight: 371 | case MCSFishEyeExpansionDirectionLeft: 372 | self.transformedItemSize = self.itemSize; 373 | break; 374 | case MCSFishEyeExpansionDirectionTop: 375 | case MCSFishEyeExpansionDirectionBottom: 376 | self.transformedItemSize = CGSizeMake(self.itemSize.height, self.itemSize.width); 377 | break; 378 | } 379 | 380 | CGFloat totalHeight = self.transformView.bounds.size.height; 381 | CGFloat itemHeight = self.transformedItemSize.height; 382 | 383 | CGFloat collapsedItemHeight = MIN(totalHeight / self.items.count, itemHeight); 384 | self.collapsedItemHeight = collapsedItemHeight; 385 | self.collapsedItemScale = self.collapsedItemHeight / itemHeight; 386 | 387 | CGFloat expansionSurplus = 0.0; 388 | 389 | for (int i = -self.expansionsNeighbors - 1; i < self.expansionsNeighbors + 1; i++) { 390 | expansionSurplus += [self scaleForOffsetFromFocusPoint:i * collapsedItemHeight] * itemHeight - collapsedItemHeight; 391 | } 392 | 393 | if (collapsedItemHeight == itemHeight) { 394 | self.centeringOffset = (totalHeight - self.items.count * collapsedItemHeight)/2.0; 395 | self.startOffset = 0.0; 396 | } else { 397 | self.centeringOffset = 0.0; 398 | self.startOffset = expansionSurplus/2.0; 399 | } 400 | } 401 | 402 | - (CGFloat)offsetForIndex:(NSUInteger)index 403 | { 404 | return (index + 0.5) * self.collapsedItemHeight + self.centeringOffset; 405 | } 406 | 407 | - (NSUInteger)indexForOffset:(CGFloat)offset 408 | { 409 | NSInteger index = floorf((offset - self.centeringOffset)/self.collapsedItemHeight); 410 | 411 | return (index >= 0 && index < [self.items count]) ? index : NSNotFound; 412 | } 413 | 414 | - (CGFloat)scaleForOffsetFromFocusPoint:(CGFloat)offset 415 | { 416 | CGFloat normalizedOffset = fabsf(offset/self.collapsedItemHeight); 417 | CGFloat scalar = 0.0; 418 | 419 | if (normalizedOffset <= 0.5) { 420 | scalar = 1.0; 421 | } else if (normalizedOffset < (self.expansionsNeighbors + 0.5)) { 422 | scalar = ((self.expansionsNeighbors + 0.5) - normalizedOffset)/self.expansionsNeighbors; 423 | } 424 | 425 | CGFloat scaledHeight = self.transformedItemSize.height * scalar + self.collapsedItemHeight * (1.0 - scalar); //lerping 426 | return scaledHeight/self.transformedItemSize.height; 427 | } 428 | 429 | - (CGFloat)translationForOffsetFromFocusPoint:(CGFloat)offset 430 | { 431 | if (!self.evadesFinger) { 432 | return 0.0f; 433 | } 434 | 435 | CGFloat normalizedOffset = fabsf(offset/self.collapsedItemHeight); 436 | CGFloat scalar = 0.0; 437 | 438 | const CGFloat NormalizedBottomRange = FingerTranslationRadius/self.collapsedItemHeight; 439 | 440 | if (normalizedOffset <= 0.5) { 441 | scalar = 1.0; 442 | } else if (normalizedOffset < NormalizedBottomRange) { 443 | scalar = 1.0 - (normalizedOffset - 0.5)/(NormalizedBottomRange - 0.5); 444 | } 445 | 446 | CGFloat val = scalar*scalar*(3.0 - 2.0*scalar); // ease in out on cubic curve 447 | 448 | 449 | return (FingerTranslation * val); 450 | } 451 | 452 | #pragma mark - Layout 453 | 454 | - (void)layoutItemsForOffset:(CGFloat)offset withAnimationDuration:(NSTimeInterval)duration 455 | { 456 | CGFloat expandedHeight = self.transformedItemSize.height; 457 | 458 | CGAffineTransform itemTransform = CGAffineTransformConcat(self.itemTransform, 459 | CGAffineTransformMakeTranslation(self.transformedItemSize.width/2.0, 0.0) 460 | ); 461 | 462 | [UIView animateWithDuration:duration animations:^{ 463 | NSEnumerationOptions options = 0; 464 | CGFloat sign = 1.0; 465 | __block CGFloat layoutOffset = -self.startOffset + self.centeringOffset; 466 | 467 | if (offset < (self.transformView.bounds.size.height)/2.0) { 468 | options = NSEnumerationReverse; 469 | sign = -1.0; 470 | layoutOffset = self.transformView.bounds.size.height + self.startOffset - self.centeringOffset; 471 | } 472 | 473 | [self.itemContainers enumerateObjectsWithOptions:options usingBlock:^(UIView *container, NSUInteger index, BOOL *stop) { 474 | CGFloat center = [self offsetForIndex:index]; 475 | CGFloat distance = offset - center; 476 | 477 | CGFloat scale, tx, ty; 478 | 479 | if (self.state == MCSFishEyeStateExpandedPassive) { 480 | if (index != self.selectedIndex) { 481 | scale = self.collapsedItemScale; 482 | tx = 0.0; 483 | } else { 484 | scale = 1.0f; 485 | tx = self.selectedItemOffset; 486 | } 487 | ty = center; 488 | } else if (self.state == MCSFishEyeStateExpandedActive) { 489 | scale = [self scaleForOffsetFromFocusPoint:distance]; 490 | tx = [self translationForOffsetFromFocusPoint:distance]; 491 | ty = layoutOffset + sign * scale * expandedHeight * 0.5; 492 | } else { 493 | scale = self.collapsedItemScale; 494 | tx = 0.0f; 495 | ty = center; 496 | } 497 | 498 | container.transform = CGAffineTransformConcat(itemTransform, [self transformWithScale:scale translation:CGPointMake(tx, ty)]); 499 | layoutOffset += sign * scale * expandedHeight; 500 | }]; 501 | }]; 502 | } 503 | 504 | - (void)layoutWithCurrentState 505 | { 506 | CGFloat offset = self.selectedIndex == NSNotFound ? 0.0f : [self offsetForIndex:self.selectedIndex]; 507 | [self layoutItemsForOffset:offset withAnimationDuration:0.2]; 508 | } 509 | 510 | #pragma mark - Convinience 511 | 512 | - (void)collapseAnimated:(BOOL)animated notifying:(BOOL)shouldNotify 513 | { 514 | self.state = MCSFishEyeStateCollapsed; 515 | [self layoutItemsForOffset:0.0 withAnimationDuration:animated ? 0.2 : 0.0]; 516 | 517 | [self setHighlightedIndex:NSNotFound]; 518 | [self setSelectedIndex:NSNotFound withDelegateCalls:YES animated:animated]; 519 | } 520 | 521 | - (CGAffineTransform)transformWithScale:(CGFloat)scale translation:(CGPoint)translation 522 | { 523 | CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scale, scale); 524 | CGAffineTransform translationTransform = CGAffineTransformMakeTranslation(translation.x, translation.y); 525 | 526 | return CGAffineTransformConcat(scaleTransform, translationTransform); 527 | } 528 | 529 | #pragma mark - Touch event handling 530 | 531 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 532 | { 533 | CGFloat offset = [self offsetFromTouchSet:touches]; 534 | 535 | self.state = MCSFishEyeStateExpandedActive; 536 | 537 | [self layoutItemsForOffset:offset withAnimationDuration:0.2]; 538 | [self setSelectedIndex:NSNotFound withDelegateCalls:YES animated:NO]; 539 | [self setHighlightedIndex:[self indexForOffset:offset]]; 540 | } 541 | 542 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 543 | { 544 | CGFloat offset = [self offsetFromTouchSet:touches]; 545 | 546 | [self layoutItemsForOffset:offset withAnimationDuration:0.07]; 547 | [self setHighlightedIndex:[self indexForOffset:offset]]; 548 | } 549 | 550 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 551 | { 552 | CGFloat offset = [self offsetFromTouchSet:touches]; 553 | NSUInteger index = [self indexForOffset:offset]; 554 | 555 | if (index != NSNotFound) { 556 | [self setState:MCSFishEyeStateExpandedPassive]; 557 | [self setHighlightedIndex:NSNotFound]; 558 | [self setSelectedIndex:index withDelegateCalls:YES animated:YES]; 559 | [self layoutItemsForOffset:offset withAnimationDuration:0.2]; 560 | } else { 561 | [self collapseAnimated:YES notifying:YES]; 562 | } 563 | } 564 | 565 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 566 | { 567 | [self collapseAnimated:YES notifying:YES]; 568 | } 569 | 570 | - (CGFloat)offsetFromTouchSet:(NSSet *)touches 571 | { 572 | UITouch *touch = [touches anyObject]; 573 | return [touch locationInView:self.transformView].y; 574 | } 575 | 576 | @end 577 | --------------------------------------------------------------------------------