[翻译] VLDContextSheet

简介:

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;

Show - 显示

- (void) longPressed: (UIGestureRecognizer *) gestureRecognizer {
    if(gestureRecognizer.state == UIGestureRecognizerStateBegan) {

        [self.contextSheet startWithGestureRecognizer: gestureRecognizer
                                               inView: self.view]; } }

Delegate method - 代理方法

- (void) contextSheet: (VLDContextSheet *) contextSheet didSelectItem: (VLDContextSheetItem *) item {
    NSLog(@"Selected item: %@", item.title); }

Hide - 隐藏

[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

目录
相关文章
|
数据可视化 Perl
|
iOS开发 Android开发
|
Java iOS开发 Spring
|
内存技术 Perl
|
iOS开发 Perl
|
数据可视化 iOS开发 Perl