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中添加如下代码:
|
2. 给control添加一些表示当前control状态的内容
|
|
3. Images vs. CoreGraphics
image 和 CoreGraphics是两种不同的描绘control的显示的方式。
两种各有优缺点,image要求美工好,CoreGraphics要求代码多。
文章介绍了用CoreGraphics的方式来做,首先要添加一个库QuartzCore.framework, 具体添加办法如下图:
然后修改CERangeSlider的m文件注意不是h文件
#import <QuartzCore/QuartzCore.h>
|
_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;
}