VLDContextSheet
效果:
A clone of the Pinterest iOS app context menu.
复制了Pinterest应用的菜单效果。
Example Usage - 使用样例
VLDContextSheetItem *item1 = [[VLDContextSheetItem alloc] initWithTitle: @"Gift" image: [UIImage imageNamed: @"gift"] highlightedImage: [UIImage imageNamed: @"gift_highlighted"]]; VLDContextSheetItem *item2 = ... VLDContextSheetItem *item3 = ... self.contextSheet = [[VLDContextSheet alloc] initWithItems: @[ item1, item2, item3 ]]; self.contextSheet.delegate = self;
- (void) longPressed: (UIGestureRecognizer *) gestureRecognizer {
if(gestureRecognizer.state == UIGestureRecognizerStateBegan) { [self.contextSheet startWithGestureRecognizer: gestureRecognizer inView: self.view]; } }
- (void) contextSheet: (VLDContextSheet *) contextSheet didSelectItem: (VLDContextSheetItem *) item {
NSLog(@"Selected item: %@", item.title); }
[self.contextSheet end];
For more info check the Example project.
你可以在示例代码里面查看更多的信息。
附录源码:
VLDContextSheetItem.h 与 VLDContextSheetItem.m
// // VLDContextSheetItem.h // // Created by Vladimir Angelov on 2/10/14. // Copyright (c) 2014 Vladimir Angelov. All rights reserved. // #import <Foundation/Foundation.h> @interface VLDContextSheetItem : NSObject - (id) initWithTitle: (NSString *) title image: (UIImage *) image highlightedImage: (UIImage *) highlightedImage; @property (strong, readonly) NSString *title; @property (strong, readonly) UIImage *image; @property (strong, readonly) UIImage *highlightedImage; @property (assign, readwrite, getter = isEnabled) BOOL enabled; @end
// // VLDContextSheetItem.m // // Created by Vladimir Angelov on 2/10/14. // Copyright (c) 2014 Vladimir Angelov. All rights reserved. // #import "VLDContextSheetItem.h" @implementation VLDContextSheetItem - (id) initWithTitle: (NSString *) title image: (UIImage *) image highlightedImage: (UIImage *) highlightedImage { self = [super init]; if(self) { _title = title; _image = image; _highlightedImage = highlightedImage; _enabled = YES; } return self; } @end
VLDContextSheetItem.h 与 VLDContextSheetItem.m
// // VLDContextSheetItem.h // // Created by Vladimir Angelov on 2/9/14. // Copyright (c) 2014 Vladimir Angelov. All rights reserved. // #import <Foundation/Foundation.h> @class VLDContextSheetItem; @interface VLDContextSheetItemView : UIView @property (strong, nonatomic) VLDContextSheetItem *item; @property (readonly) BOOL isHighlighted; - (void) setHighlighted: (BOOL) highlighted animated: (BOOL) animated; @end
// // VLDContextSheetItemView.m, // // Created by Vladimir Angelov on 2/9/14. // Copyright (c) 2014 Vladimir Angelov. All rights reserved. // #import "VLDContextSheetItemView.h" #import "VLDContextSheetItem.h" #import <CoreImage/CoreImage.h> static const NSInteger VLDTextPadding = 5; @interface VLDContextSheetItemView () @property (nonatomic, strong) UIImageView *imageView; @property (nonatomic, strong) UIImageView *highlightedImageView; @property (nonatomic, strong) UILabel *label; @property (nonatomic, assign) NSInteger labelWidth; @end @implementation VLDContextSheetItemView @synthesize item = _item; - (id) initWithFrame: (CGRect) frame { self = [super initWithFrame: CGRectMake(0, 0, 50, 83)]; if(self) { [self createSubviews]; } return self; } - (void) createSubviews { _imageView = [[UIImageView alloc] init]; [self addSubview: _imageView]; _highlightedImageView = [[UIImageView alloc] init]; _highlightedImageView.alpha = 0.0; [self addSubview: _highlightedImageView]; _label = [[UILabel alloc] init]; _label.clipsToBounds = YES; _label.font = [UIFont systemFontOfSize: 10]; _label.textAlignment = NSTextAlignmentCenter; _label.layer.cornerRadius = 7; _label.backgroundColor = [UIColor colorWithWhite: 0.0 alpha: 0.4]; _label.textColor = [UIColor whiteColor]; _label.alpha = 0.0; [self addSubview: _label]; } - (void) layoutSubviews { [super layoutSubviews]; self.imageView.frame = CGRectMake(0, (self.frame.size.height - self.frame.size.width) / 2, self.frame.size.width, self.frame.size.width); self.highlightedImageView.frame = self.imageView.frame; self.label.frame = CGRectMake((self.frame.size.width - self.labelWidth) / 2.0, 0, self.labelWidth, 14); } - (void) setItem:(VLDContextSheetItem *)item { _item = item; [self updateImages]; [self updateLabelText]; } - (void) updateImages { self.imageView.image = self.item.image; self.highlightedImageView.image = self.item.highlightedImage; self.imageView.alpha = self.item.isEnabled ? 1.0 : 0.3; } - (void) updateLabelText { self.label.text = self.item.title; self.labelWidth = 2 * VLDTextPadding + ceil([self.label.text sizeWithAttributes: @{ NSFontAttributeName: self.label.font }].width); [self setNeedsDisplay]; } - (void) setHighlighted: (BOOL) highlighted animated: (BOOL) animated { if (!self.item.isEnabled) { return; } _isHighlighted = highlighted; [UIView animateWithDuration: animated ? 0.3 : 0.0 delay: 0.0 options: UIViewAnimationOptionCurveEaseInOut animations:^{ self.highlightedImageView.alpha = (highlighted ? 1.0 : 0.0); self.imageView.alpha = 1 - self.highlightedImageView.alpha; self.label.alpha = self.highlightedImageView.alpha; } completion: nil]; } @end
VLDContextSheet.h 与 VLDContextSheet.m
// // VLDContextSheet.h // // Created by Vladimir Angelov on 2/7/14. // Copyright (c) 2014 Vladimir Angelov. All rights reserved. // #import <Foundation/Foundation.h> @class VLDContextSheet; @class VLDContextSheetItem; @protocol VLDContextSheetDelegate <NSObject> - (void) contextSheet: (VLDContextSheet *) contextSheet didSelectItem: (VLDContextSheetItem *) item; @end @interface VLDContextSheet : UIView @property (assign, nonatomic) NSInteger radius; @property (assign, nonatomic) CGFloat rotation; @property (assign, nonatomic) CGFloat rangeAngle; @property (strong, nonatomic) NSArray *items; @property (assign, nonatomic) id<VLDContextSheetDelegate> delegate; - (id) initWithItems: (NSArray *) items; - (void) startWithGestureRecognizer: (UIGestureRecognizer *) gestureRecognizer inView: (UIView *) view; - (void) end; @end
// // VLDContextSheet.m // // Created by Vladimir Angelov on 2/7/14. // Copyright (c) 2014 Vladimir Angelov. All rights reserved. // #import "VLDContextSheetItemView.h" #import "VLDContextSheet.h" typedef struct { CGRect rect; CGFloat rotation; } VLDZone; static const NSInteger VLDMaxTouchDistanceAllowance = 40; static const NSInteger VLDZonesCount = 10; static inline VLDZone VLDZoneMake(CGRect rect, CGFloat rotation) { VLDZone zone; zone.rect = rect; zone.rotation = rotation; return zone; } static CGFloat VLDVectorDotProduct(CGPoint vector1, CGPoint vector2) { return vector1.x * vector2.x + vector1.y * vector2.y; } static CGFloat VLDVectorLength(CGPoint vector) { return sqrt(vector.x * vector.x + vector.y * vector.y); } static CGRect VLDOrientedScreenBounds() { CGRect bounds = [UIScreen mainScreen].bounds; if(UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation) && bounds.size.width < bounds.size.height) { bounds.size = CGSizeMake(bounds.size.height, bounds.size.width); } return bounds; } @interface VLDContextSheet () @property (strong, nonatomic) NSArray *itemViews; @property (strong, nonatomic) UIView *centerView; @property (strong, nonatomic) UIView *backgroundView; @property (strong, nonatomic) VLDContextSheetItemView *selectedItemView; @property (assign, nonatomic) BOOL openAnimationFinished; @property (assign, nonatomic) CGPoint touchCenter; @property (strong, nonatomic) UIGestureRecognizer *starterGestureRecognizer; @end @implementation VLDContextSheet { VLDZone zones[VLDZonesCount]; } - (id) initWithFrame: (CGRect) frame { return [self initWithItems: nil]; } - (id) initWithItems: (NSArray *) items { self = [super initWithFrame: VLDOrientedScreenBounds()]; if(self) { _items = items; _radius = 100; _rangeAngle = M_PI / 1.6; [self createSubviews]; } return self; } - (void) dealloc { [self.starterGestureRecognizer removeTarget: self action: @selector(gestureRecognizedStateObserver:)]; } - (void) createSubviews { _backgroundView = [[UIView alloc] initWithFrame: CGRectZero]; _backgroundView.backgroundColor = [UIColor colorWithWhite: 0 alpha: 0.6]; [self addSubview: self.backgroundView]; _itemViews = [[NSMutableArray alloc] init]; for(VLDContextSheetItem *item in _items) { VLDContextSheetItemView *itemView = [[VLDContextSheetItemView alloc] init]; itemView.item = item; [self addSubview: itemView]; [(NSMutableArray *) _itemViews addObject: itemView]; } VLDContextSheetItemView *sampleItemView = _itemViews[0]; _centerView = [[UIView alloc] initWithFrame: CGRectMake(0, 0, sampleItemView.frame.size.width, sampleItemView.frame.size.width)]; _centerView.layer.cornerRadius = 25; _centerView.layer.borderWidth = 2; _centerView.layer.borderColor = [UIColor grayColor].CGColor; [self addSubview: _centerView]; } - (void) layoutSubviews { [super layoutSubviews]; self.backgroundView.frame = self.bounds; } - (void) setCenterViewHighlighted: (BOOL) highlighted { _centerView.backgroundColor = highlighted ? [UIColor colorWithWhite: 0.5 alpha: 0.4] : nil; } - (void) createZones { CGRect screenRect = self.bounds; NSInteger rowHeight1 = 120; zones[0] = VLDZoneMake(CGRectMake(0, 0, 70, rowHeight1), 0.8); zones[1] = VLDZoneMake(CGRectMake(zones[0].rect.size.width, 0, 40, rowHeight1), 0.4); zones[2] = VLDZoneMake(CGRectMake(zones[1].rect.origin.x + zones[1].rect.size.width, 0, screenRect.size.width - 2 *(zones[0].rect.size.width + zones[1].rect.size.width), rowHeight1), 0); zones[3] = VLDZoneMake(CGRectMake(zones[2].rect.origin.x + zones[2].rect.size.width, 0, zones[1].rect.size.width, rowHeight1), -zones[1].rotation); zones[4] = VLDZoneMake(CGRectMake(zones[3].rect.origin.x + zones[3].rect.size.width, 0, zones[0].rect.size.width, rowHeight1), -zones[0].rotation); NSInteger rowHeight2 = screenRect.size.height - zones[0].rect.size.height; zones[5] = VLDZoneMake(CGRectMake(0, zones[0].rect.size.height, zones[0].rect.size.width, rowHeight2), M_PI - zones[0].rotation); zones[6] = VLDZoneMake(CGRectMake(zones[5].rect.size.width, zones[5].rect.origin.y, zones[1].rect.size.width, rowHeight2), M_PI - zones[1].rotation); zones[7] = VLDZoneMake(CGRectMake(zones[6].rect.origin.x + zones[6].rect.size.width, zones[5].rect.origin.y, zones[2].rect.size.width, rowHeight2), M_PI - zones[2].rotation); zones[8] = VLDZoneMake(CGRectMake(zones[7].rect.origin.x + zones[7].rect.size.width, zones[5].rect.origin.y, zones[3].rect.size.width, rowHeight2), M_PI - zones[3].rotation); zones[9] = VLDZoneMake(CGRectMake(zones[8].rect.origin.x + zones[8].rect.size.width, zones[5].rect.origin.y, zones[4].rect.size.width, rowHeight2), M_PI - zones[4].rotation); } /* Only used for testing the touch zones */ - (void) drawZones { for(int i = 0; i < VLDZonesCount; i++) { UIView *zoneView = [[UIView alloc] initWithFrame: zones[i].rect]; CGFloat hue = ( arc4random() % 256 / 256.0 ); CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; UIColor *color = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; zoneView.backgroundColor = color; [self addSubview: zoneView]; } } - (void) updateItemView: (UIView *) itemView touchDistance: (CGFloat) touchDistance animated: (BOOL) animated { if(!animated) { [self updateItemViewNotAnimated: itemView touchDistance: touchDistance]; } else { [UIView animateWithDuration: 0.4 delay: 0 usingSpringWithDamping: 0.45 initialSpringVelocity: 7.5 options: UIViewAnimationOptionBeginFromCurrentState animations: ^{ [self updateItemViewNotAnimated: itemView touchDistance: touchDistance]; } completion: nil]; } } - (void) updateItemViewNotAnimated: (UIView *) itemView touchDistance: (CGFloat) touchDistance { NSInteger itemIndex = [self.itemViews indexOfObject: itemView]; CGFloat angle = -0.65 + self.rotation + itemIndex * (self.rangeAngle / self.itemViews.count); CGFloat resistanceFactor = 1.0 / (touchDistance > 0 ? 6.0 : 3.0); itemView.center = CGPointMake(self.touchCenter.x + (self.radius + touchDistance * resistanceFactor) * sin(angle), self.touchCenter.y + (self.radius + touchDistance * resistanceFactor) * cos(angle)); CGFloat scale = 1 + 0.2 * (fabs(touchDistance) / self.radius); itemView.transform = CGAffineTransformMakeScale(scale, scale); } - (void) openItemsFromCenterView { self.openAnimationFinished = NO; for(int i = 0; i < self.itemViews.count; i++) { VLDContextSheetItemView *itemView = self.itemViews[i]; itemView.transform = CGAffineTransformIdentity; itemView.center = self.touchCenter; [itemView setHighlighted: NO animated: NO]; [UIView animateWithDuration: 0.5 delay: i * 0.01 usingSpringWithDamping: 0.45 initialSpringVelocity: 7.5 options: 0 animations: ^{ [self updateItemViewNotAnimated: itemView touchDistance: 0.0]; } completion: ^(BOOL finished) { self.openAnimationFinished = YES; }]; } } - (void) closeItemsToCenterView { [UIView animateWithDuration: 0.1 delay: 0.0 options: UIViewAnimationOptionCurveEaseInOut animations:^{ self.alpha = 0.0; } completion:^(BOOL finished) { [self removeFromSuperview]; self.alpha = 1.0; }]; } - (void) startWithGestureRecognizer: (UIGestureRecognizer *) gestureRecognizer inView: (UIView *) view { [view addSubview: self]; self.frame = VLDOrientedScreenBounds(); [self createZones]; self.starterGestureRecognizer = gestureRecognizer; self.touchCenter = [self.starterGestureRecognizer locationInView: self]; self.centerView.center = self.touchCenter; self.selectedItemView = nil; [self setCenterViewHighlighted: YES]; self.rotation = [self rotationForCenter: self.centerView.center]; [self openItemsFromCenterView]; [self.starterGestureRecognizer addTarget: self action: @selector(gestureRecognizedStateObserver:)]; } - (CGFloat) rotationForCenter: (CGPoint) center { for(NSInteger i = 0; i < 10; i++) { VLDZone zone = zones[i]; if(CGRectContainsPoint(zone.rect, center)) { return zone.rotation; } } return 0; } - (void) gestureRecognizedStateObserver: (UIGestureRecognizer *) gestureRecognizer { if(self.openAnimationFinished && gestureRecognizer.state == UIGestureRecognizerStateChanged) { CGPoint touchPoint = [gestureRecognizer locationInView: self]; [self updateItemViewsForTouchPoint: touchPoint]; } else if(gestureRecognizer.state == UIGestureRecognizerStateEnded || gestureRecognizer.state == UIGestureRecognizerStateCancelled) { if(gestureRecognizer.state == UIGestureRecognizerStateCancelled) { self.selectedItemView = nil; } [self end]; } } - (CGFloat) signedTouchDistanceForTouchVector: (CGPoint) touchVector itemView: (UIView *) itemView { CGFloat touchDistance = VLDVectorLength(touchVector); CGPoint oldCenter = itemView.center; CGAffineTransform oldTransform = itemView.transform; [self updateItemViewNotAnimated: itemView touchDistance: self.radius + 40]; if(!CGRectContainsRect(self.bounds, itemView.frame)) { touchDistance = -touchDistance; } itemView.center = oldCenter; itemView.transform = oldTransform; return touchDistance; } - (void) updateItemViewsForTouchPoint: (CGPoint) touchPoint { CGPoint touchVector = {touchPoint.x - self.touchCenter.x, touchPoint.y - self.touchCenter.y}; VLDContextSheetItemView *itemView = [self itemViewForTouchVector: touchVector]; CGFloat touchDistance = [self signedTouchDistanceForTouchVector: touchVector itemView: itemView]; if(fabs(touchDistance) <= VLDMaxTouchDistanceAllowance) { self.centerView.center = CGPointMake(self.touchCenter.x + touchVector.x, self.touchCenter.y + touchVector.y); [self setCenterViewHighlighted: YES]; } else { [self setCenterViewHighlighted: NO]; [UIView animateWithDuration: 0.4 delay: 0 usingSpringWithDamping: 0.35 initialSpringVelocity: 7.5 options: UIViewAnimationOptionBeginFromCurrentState animations: ^{ self.centerView.center = self.touchCenter; } completion: nil]; } if(touchDistance > self.radius + VLDMaxTouchDistanceAllowance) { [itemView setHighlighted: NO animated: YES]; [self updateItemView: itemView touchDistance: 0.0 animated: YES]; self.selectedItemView = nil; return; } if(itemView != self.selectedItemView) { [self.selectedItemView setHighlighted: NO animated: YES]; [self updateItemView: self.selectedItemView touchDistance: 0.0 animated: YES]; [self updateItemView: itemView touchDistance: touchDistance animated: YES]; [self bringSubviewToFront: itemView]; } else { [self updateItemView: itemView touchDistance: touchDistance animated: NO]; } if(fabs(touchDistance) > VLDMaxTouchDistanceAllowance) { [itemView setHighlighted: YES animated: YES]; } self.selectedItemView = itemView; } - (VLDContextSheetItemView *) itemViewForTouchVector: (CGPoint) touchVector { CGFloat maxCosOfAngle = -2; VLDContextSheetItemView *resultItemView = nil; for(int i = 0; i < self.itemViews.count; i++) { VLDContextSheetItemView *itemView = self.itemViews[i]; CGPoint itemViewVector = { itemView.center.x - self.touchCenter.x, itemView.center.y - self.touchCenter.y }; CGFloat cosOfAngle = VLDVectorDotProduct(itemViewVector, touchVector) / VLDVectorLength(itemViewVector); if(cosOfAngle > maxCosOfAngle) { maxCosOfAngle = cosOfAngle; resultItemView = itemView; } } return resultItemView; } - (void) end { [self.starterGestureRecognizer removeTarget: self action: @selector(gestureRecognizedStateObserver:)]; if(self.selectedItemView && self.selectedItemView.isHighlighted) { [self.delegate contextSheet: self didSelectItem: self.selectedItemView.item]; } [self closeItemsToCenterView]; } @end
控制器源码:
// // VLDExampleViewController.h // VLDContextSheetExample // // Created by Vladimir Angelov on 11/2/14. // Copyright (c) 2014 Vladimir Angelov. All rights reserved. // #import <Foundation/Foundation.h> #import "VLDContextSheet.h" @interface VLDExampleViewController : UIViewController <VLDContextSheetDelegate> @property (strong, nonatomic) VLDContextSheet *contextSheet; @end
// // VLDExampleViewController.m // VLDContextSheetExample // // Created by Vladimir Angelov on 11/2/14. // Copyright (c) 2014 Vladimir Angelov. All rights reserved. // #import "VLDExampleViewController.h" #import "VLDContextSheetItem.h" @implementation VLDExampleViewController - (void) viewDidLoad { [super viewDidLoad]; [self createContextSheet]; self.view.backgroundColor = [UIColor blackColor]; UIGestureRecognizer *gestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget: self action: @selector(longPressed:)]; [self.view addGestureRecognizer: gestureRecognizer]; } - (void) createContextSheet { VLDContextSheetItem *item1 = [[VLDContextSheetItem alloc] initWithTitle: @"Gift" image: [UIImage imageNamed: @"gift"] highlightedImage: [UIImage imageNamed: @"gift_highlighted"]]; VLDContextSheetItem *item2 = [[VLDContextSheetItem alloc] initWithTitle: @"Add to" image: [UIImage imageNamed: @"add"] highlightedImage: [UIImage imageNamed: @"add_highlighted"]]; VLDContextSheetItem *item3 = [[VLDContextSheetItem alloc] initWithTitle: @"Share" image: [UIImage imageNamed: @"share"] highlightedImage: [UIImage imageNamed: @"share_highlighted"]]; self.contextSheet = [[VLDContextSheet alloc] initWithItems: @[ item1, item2, item3 ]]; self.contextSheet.delegate = self; } - (void) contextSheet: (VLDContextSheet *) contextSheet didSelectItem: (VLDContextSheetItem *) item { NSLog(@"Selected item: %@", item.title); } - (void) longPressed: (UIGestureRecognizer *) gestureRecognizer { if(gestureRecognizer.state == UIGestureRecognizerStateBegan) { [self.contextSheet startWithGestureRecognizer: gestureRecognizer inView: self.view]; } } - (void) willRotateToInterfaceOrientation: (UIInterfaceOrientation) toInterfaceOrientation duration: (NSTimeInterval) duration { [super willRotateToInterfaceOrientation: toInterfaceOrientation duration: duration]; [self.contextSheet end]; } @end