http://www.raywenderlich.com/36288/how-to-make-a-custom-control
http://hubpages.com/hub/iOS-Create-Custom-Buttons-and-Controls 本文主要是对上门的链接的摘要。
下面是apple control的一个hierarchy(如果需要查看更详细的hierachy参照UIKit Framework Reference):从上图中UIControl可以相应一些event,是做contol很好的开始。下面就是一个做带两个按钮的slider(效果如下图)的很好的例子:
1. 首先从UIControl继承一个class叫CERangeSlider,当然我们希望能够一边改进这个control一边看到改进的结果,所以我们在能显示的view中添加如下代码:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSUInteger margin = 20;
CGRect sliderFrame = CGRectMake(margin, margin, self.view.frame.size.width - margin * 2, 30);
_rangeSlider = [[CERangeSlider alloc] initWithFrame:sliderFrame];
_rangeSlider.backgroundColor = [UIColor redColor];
[self.view addSubview:_rangeSlider];
} |
然后就看到如下图片
2. 给control添加一些表示当前control状态的内容
@property (nonatomic) float maximumValue;
@property (nonatomic) float minimumValue;
@property (nonatomic) float upperValue;
@property (nonatomic) float lowerValue; |
并给一些初始数值
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
_maximumValue = 10.0;
_minimumValue = 0.0;
_upperValue = 8.0;
_lowerValue = 2.0;
}
return self;
} |
3. Images vs. CoreGraphics
image 和 CoreGraphics是两种不同的描绘control的显示的方式。两种各有优缺点,image要求美工好,CoreGraphics要求代码多。文章介绍了用CoreGraphics的方式来做,首先要添加一个库QuartzCore.framework, 具体添加办法如下图:
然后修改CERangeSlider的m文件注意不是h文件
#import <QuartzCore/QuartzCore.h>
@implementation CERangeSlider
{
CALayer* _trackLayer;
CALayer* _upperKnobLayer;
CALayer* _lowerKnobLayer;
float _knobWidth;
float _useableTrackLength;
} |
然后在initial Frame中添加如下代码来初始化这些layer:
_trackLayer = [CALayer layer];
_trackLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layer addSublayer:_trackLayer];
_upperKnobLayer = [CALayer layer];
_upperKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_upperKnobLayer];
_lowerKnobLayer = [CALayer layer];
_lowerKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_lowerKnobLayer];
[self setLayerFrames];
然后继续添加一些初始化的功能
- (void) setLayerFrames
{
_trackLayer.frame = CGRectInset(self.bounds, 0, self.bounds.size.height / 3.5);
[_trackLayer setNeedsDisplay];
_knobWidth = self.bounds.size.height;
_useableTrackLength = self.bounds.size.width - _knobWidth;
float upperKnobCentre = [self positionForValue:_upperValue];
_upperKnobLayer.frame = CGRectMake(upperKnobCentre - _knobWidth / 2, 0, _knobWidth, _knobWidth);
float lowerKnobCentre = [self positionForValue:_lowerValue];
_lowerKnobLayer.frame = CGRectMake(lowerKnobCentre - _knobWidth / 2, 0, _knobWidth, _knobWidth);
[_upperKnobLayer setNeedsDisplay];
[_lowerKnobLayer setNeedsDisplay];
}
- (float) positionForValue:(float)value // 坐标映射
{
return _useableTrackLength * (value - _minimumValue) /
(_maximumValue - _minimumValue) + (_knobWidth / 2);
}然后运行会有下面的结果 4. 下面要添加一些互动的功能想从CALayer继承一个class CERangeSliderKnobLayer。 #import <QuartzCore/QuartzCore.h>
@class CERangeSlider;
@interface CERangeSliderKnobLayer : CALayer
@property BOOL highlighted;
@property (weak) CERangeSlider* slider;
@end | 下面把原来的两个layer替换掉CERangeSliderKnobLayer* _upperKnobLayer;
CERangeSliderKnobLayer* _lowerKnobLayer; |
init frame里也做如下修改 _upperKnobLayer = [CERangeSliderKnobLayer layer];
_upperKnobLayer.slider = self;
_upperKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_upperKnobLayer];
_lowerKnobLayer = [CERangeSliderKnobLayer layer];
_lowerKnobLayer.slider = self;
_lowerKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_lowerKnobLayer]; |
再在CERangeSlide添加触摸相应的内容,分为begin continue end三部分: - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event // 首先要筛选出相应的区域
{
_previousTouchPoint = [touch locationInView:self];
// hit test the knob layers
if(CGRectContainsPoint(_lowerKnobLayer.frame, _previousTouchPoint))
{
_lowerKnobLayer.highlighted = YES;
[_lowerKnobLayer setNeedsDisplay];
}
else if(CGRectContainsPoint(_upperKnobLayer.frame, _previousTouchPoint))
{
_upperKnobLayer.highlighted = YES;
[_upperKnobLayer setNeedsDisplay];
}
return _upperKnobLayer.highlighted || _lowerKnobLayer.highlighted;
} |
|
#define BOUND(VALUE, UPPER, LOWER) MIN(MAX(VALUE, LOWER), UPPER) - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event // 修改数值{ CGPoint touchPoint = [touch locationInView:self]; // 1. determine by how much the user has dragged float delta = touchPoint.x - _previousTouchPoint.x; float valueDelta = (_maximumValue - _minimumValue) * delta / _useableTrackLength; _previousTouchPoint = touchPoint; // 2. update the values if (_lowerKnobLayer.highlighted) { _lowerValue += valueDelta; _lowerValue = BOUND(_lowerValue, _upperValue, _minimumValue); } if (_upperKnobLayer.highlighted) { _upperValue += valueDelta; _upperValue = BOUND(_upperValue, _maximumValue, _lowerValue); } // 3. Update the UI state [CATransaction begin]; [CATransaction setDisableActions:YES] ; [self setLayerFrames]; [CATransaction commit]; return YES;} |
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{ _lowerKnobLayer.highlighted = _upperKnobLayer.highlighted = NO; [_lowerKnobLayer setNeedsDisplay]; [_upperKnobLayer setNeedsDisplay];} 5. control可以拖动了,那怎样告诉使用control的view当前的状态呢?有两种pattern可以采用delegate和target-action,一般认为前一种拓展更好,后一种更加系统,等于是内置的,这里我们主要用后面一种。 在 continueTrackingWithTouch:withEvent:中添加如下代码: [self sendActionsForControlEvents:UIControlEventValueChanged]; 然后再在我们显示的主程序中添加如下代码:[_rangeSlider addTarget:self action:@selector(slideValueChanged:) forControlEvents:UIControlEventValueChanged]; |
- (void)slideValueChanged:(id)control{ NSLog(@"Slider value changed: (%.2f,%.2f)", _rangeSlider.lowerValue, _rangeSlider.upperValue);} 至此主要的功能都有了。6. 利用CoreGraphic修改外观再添加一个 CERangeSliderTrackLayer #import <QuartzCore/QuartzCore.h> @class CERangeSlider; @interface CERangeSliderTrackLayer : CALayer @property (weak) CERangeSlider* slider; @end | 同样对之前设置的_trackLayer做和之前其他的layer一样的修改。 然后在CERangeSlider.h 添加 @property (nonatomic) UIColor* trackColour;@property (nonatomic) UIColor* trackHighlightColour;@property (nonatomic) UIColor* knobColour;@property (nonatomic) float curvaceousness; - (float) positionForValue:(float)value; 在CERangeSlider.m initWithFrame中添加_trackHighlightColour = [UIColor colorWithRed:0.0 green:0.45 blue:0.94 alpha:1.0];_trackColour = [UIColor colorWithWhite:0.9 alpha:1.0];_knobColour = [UIColor whiteColor];_curvaceousness = 1.0;_maximumValue = 10.0;_minimumValue = 0.0; | 然后在CERangeSliderTrackLayer.m中添加- (void)drawInContext:(CGContextRef)ctx{ // clip float cornerRadius = self.bounds.size.height * self.slider.curvaceousness / 2.0; UIBezierPath *switchOutline = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:cornerRadius]; CGContextAddPath(ctx, switchOutline.CGPath); CGContextClip(ctx); // 1) fill the track CGContextSetFillColorWithColor(ctx, self.slider.trackColour.CGColor); CGContextAddPath(ctx, switchOutline.CGPath); CGContextFillPath(ctx); // 2) fill the highlighed range CGContextSetFillColorWithColor(ctx, self.slider.trackHighlightColour.CGColor); float lower = [self.slider positionForValue:self.slider.lowerValue]; float upper = [self.slider positionForValue:self.slider.upperValue]; CGContextFillRect(ctx, CGRectMake(lower, 0, upper - lower, self.bounds.size.height)); // 3) add a highlight over the track CGRect highlight = CGRectMake(cornerRadius/2, self.bounds.size.height/2, self.bounds.size.width - cornerRadius, self.bounds.size.height/2); UIBezierPath *highlightPath = [UIBezierPath bezierPathWithRoundedRect:highlight cornerRadius:highlight.size.height * self.slider.curvaceousness / 2.0]; CGContextAddPath(ctx, highlightPath.CGPath); CGContextSetFillColorWithColor(ctx, [UIColor colorWithWhite:1.0 alpha:0.4].CGColor); CGContextFillPath(ctx); // 4) inner shadow CGContextSetShadowWithColor(ctx, CGSizeMake(0, 2.0), 3.0, [UIColor grayColor].CGColor); CGContextAddPath(ctx, switchOutline.CGPath); CGContextSetStrokeColorWithColor(ctx, [UIColor grayColor].CGColor); CGContextStrokePath(ctx); // 5) outline the track CGContextAddPath(ctx, switchOutline.CGPath); CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor); CGContextSetLineWidth(ctx, 0.5); CGContextStrokePath(ctx); } | 显示如下 |
然后再修改其他的layer CERangeSliderKnobLayer.m,
- (void)drawInContext:(CGContextRef)ctx{ CGRect knobFrame = CGRectInset(self.bounds, 2.0, 2.0); UIBezierPath *knobPath = [UIBezierPath bezierPathWithRoundedRect:knobFrame cornerRadius:knobFrame.size.height * self.slider.curvaceousness / 2.0]; // 1) fill - with a subtle shadow CGContextSetShadowWithColor(ctx, CGSizeMake(0, 1), 1.0, [UIColor grayColor].CGColor); CGContextSetFillColorWithColor(ctx, self.slider.knobColour.CGColor); CGContextAddPath(ctx, knobPath.CGPath); CGContextFillPath(ctx); // 2) outline CGContextSetStrokeColorWithColor(ctx, [UIColor grayColor].CGColor); CGContextSetLineWidth(ctx, 0.5); CGContextAddPath(ctx, knobPath.CGPath); CGContextStrokePath(ctx); // 3) inner gradient CGRect rect = CGRectInset(knobFrame, 2.0, 2.0); UIBezierPath *clipPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:rect.size.height * self.slider.curvaceousness / 2.0]; CGGradientRef myGradient; CGColorSpaceRef myColorspace; size_t num_locations = 2; CGFloat locations[2] = { 0.0, 1.0 }; CGFloat components[8] = { 0.0, 0.0, 0.0 , 0.15, // Start color 0.0, 0.0, 0.0, 0.05 }; // End color myColorspace = CGColorSpaceCreateDeviceRGB(); myGradient = CGGradientCreateWithColorComponents (myColorspace, components, locations, num_locations); CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect)); CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect)); CGContextSaveGState(ctx); CGContextAddPath(ctx, clipPath .CGPath); CGContextClip(ctx); CGContextDrawLinearGradient(ctx, myGradient, startPoint, endPoint, 0); CGGradientRelease(myGradient); CGColorSpaceRelease(myColorspace); CGContextRestoreGState(ctx); // 4) highlight if (self.highlighted) { // fill CGContextSetFillColorWithColor(ctx, [UIColor colorWithWhite:0.0 alpha:0.1].CGColor); CGContextAddPath(ctx, knobPath.CGPath); CGContextFillPath(ctx); }} |
7. 之前都是参数相应control的变化,现在让control相应参数的变化
下面的代码比较tricky但是也是一种很好的模式。
#define GENERATE_SETTER(PROPERTY, TYPE, SETTER, UPDATER) \- (void)SETTER:(TYPE)PROPERTY { \ if (_##PROPERTY != PROPERTY) { \ _##PROPERTY = PROPERTY; \ [self UPDATER]; \ } \}
GENERATE_SETTER(trackHighlightColour, UIColor*, setTrackHighlightColour, redrawLayers) GENERATE_SETTER(trackColour, UIColor*, setTrackColour, redrawLayers) GENERATE_SETTER(curvaceousness, float, setCurvaceousness, redrawLayers) GENERATE_SETTER(knobColour, UIColor*, setKnobColour, redrawLayers) GENERATE_SETTER(maximumValue, float, setMaximumValue, setLayerFrames) GENERATE_SETTER(minimumValue, float, setMinimumValue, setLayerFrames) GENERATE_SETTER(lowerValue, float, setLowerValue, setLayerFrames) GENERATE_SETTER(upperValue, float, setUpperValue, setLayerFrames) - (void) redrawLayers{ [_upperKnobLayer setNeedsDisplay]; [_lowerKnobLayer setNeedsDisplay]; [_trackLayer setNeedsDisplay];} |
之后再设置一下主调用control的view
[self performSelector:@selector(updateState) withObject:nil afterDelay:1.0f];
- (void)updateState{ _rangeSlider.trackHighlightColour = [UIColor redColor]; _rangeSlider.curvaceousness = 0.0;}