10 вещей, которых вы не знали о Java
перевод
54
UITextField
, UIButton
и UISwitch
. Используя всю мощь стандартных элементов управления, можно создать множество различных вариантов интерфейса. Как бы то ни было, иногда возникает необходимость в чем-то, что реализовать силами стандартных компонентов невозможно.UISlider
, один из которых устанавливает минимальную цену, другой — максимальную, как показано на скриншоте:UIView
. И если в контексте одного конкретного приложения такое решение оправдывает себя, то повторное использование его в других разработках потребует определенных усилий. Гораздо лучшей идеей будет сделать этот компонент универсальным, доступным для использования в любом приложении. В этом и заключается смысл кастомных элементов управления.UIKit Framework
. Также, как и стандартные компоненты, они должны быть полностью универсальными и настраиваемыми под любые нужды. За последнее время сформировалось целое сообщество разработчиков, выкладывающих в открытый доступ свои компоненты.RangeSlider
, который решает рассмотренную выше задачу. Будут затронуты такие моменты, как расширение существующих компонентов, разработка API и даже выкладывание вашего творения в публичный доступ.UIView
.Apple UIKit
, вы заметите, что множество элементов, таких как UILabel
и UIWebView
напрямую наследуют UIView
. Но, как бы то ни было, есть и такие элементы, которые наследуют UIControl
, как показано на этом рисунке:Примечание: С подробной иерархией элементов интерфейса можно ознакомиться здесь: UIKit Framework Reference.
UIControl
реализует шаблон Target-Action, по сути являющийся способом уведомления об изменениях компонента. Также у UIControl
есть несколько свойств, относящихся к контролю состояния объекта. Для создания нашего компонента вы будете использовать именно такой шаблон, так что UIControl
послужит отличным стартом.#import "CERangeSlider.h"
@implementation CEViewController
{
CERangeSlider* _rangeSlider;
}
viewDidLoad
следующим блоком кода:- (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];
}
Примечание: API вашего компонента определяет методы и свойства, которые вы собираетесь предоставить другим разработчикам. Далее в статье вы прочитаете о структуре API — а пока что оставайтесь на связи!
@interface
и @end:
@property (nonatomic) float maximumValue;
@property (nonatomic) float minimumValue;
@property (nonatomic) float upperValue;
@property (nonatomic) float lowerValue;
initWithFrame:
, сгенерированный XCode, и замените его следующим кодом:- (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;
}
UIImage
.Примечание: Что любопытно, Apple в своих компонентах предпочитает использовать графические ресурсы. Скорее всего, это происходит из-за того, что они установили стандартные размеры для каждого элемента и не дают возможности его полной кастомизации.
#import <QuartzCore/QuartzCore.h>
@implementation
:@implementation CERangeSlider
{
CALayer* _trackLayer;
CALayer* _upperKnobLayer;
CALayer* _lowerKnobLayer;
float _knobWidth;
float _useableTrackLength;
}
_trackLayer
, _upperKnobLayer
и _lowerKnobLayer
будут использованы для отображения различных элементов вашего компонента. Две переменные _knobWidth
и _useableTrackLength
используются для задания параметров этих элементов.initWithFrame:
и добавьте следующий код в блок if (self) { }
:_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);
}
setLayerFrames
устанавливает размеры для обоих ползунков и полоски прогресса, основываясь на текущих значениях слайдера. positionForValue
привязывает значение к координатам экрана, используя простую пропорцию для масштабирования расстояния между максимальным и минимальным значениями компонента.CALayer
и назовите CERangeSliderKnobLayer.#import <QuartzCore/QuartzCore.h>
@class CERangeSlider;
@interface CERangeSliderKnobLayer : CALayer
@property BOOL highlighted;
@property (weak) CERangeSlider* slider;
@end
#import
:#import "CERangeSliderKnobLayer.h"
_upperKnobLayer
и _lowerKnobLayer
в блоке @implementation
:CERangeSliderKnobLayer* _upperKnobLayer;
CERangeSliderKnobLayer* _lowerKnobLayer;
initWithFrame:
и замените код инициализации upperKnobLayer
и lowerKnobLayer
на следующий блок кода:_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];
slider
в значение self
. Запустите свой проект и удостоверьтесь, что все выглядит так же, как на скриншоте:CGPoint _previousTouchPoint;
UIControl
предоставляет несколько методов для отслеживания нажатий. Сабклассы UIControl
могут переопределять эти методы для реализации своей собственной логики. В вашем элементе управления вы переопределите следующие три метода: beginTrackingWithTouch
, continueTrackingWithTouch
и endTrackingWithTouch
.- (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;
}
setNeedsDisplay
позволяет удостовериться в том, что слои были обновлены — далее вы поймете, почему это важно.#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;
}
delta
— количество пикселей на которые был передвинут палец. Затем вы конвертируете их в зависимости от минимального и максимального значений компонента.BOUND
, который более удобен для чтения, нежели MIN/MAX
.disabledActions
в CATransaction
. Это позволяет удостовериться, что изменения границ каждого слоя применяются незамедлительно и не анимируются. В конце вызывается метод setLayerFrames
, передвигающий ползунок в нужное место.- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
_lowerKnobLayer.highlighted = _upperKnobLayer.highlighted = NO;
[_lowerKnobLayer setNeedsDisplay];
[_upperKnobLayer setNeedsDisplay];
}
UIKit
, вы увидите, что они не используют NSNotification или KVO, так что для совместимости с UIKit
мы должны отказаться от этих двух вариантов. Два других шаблона — делегирование и Target-Action — очень часто используются в UIKit
.UIControl
. При изменении состояния компонента цель (target) уведомляется о действии (action), которое описывается одним из enum значений UIControlEvents
. Вы можете предоставлять множество целей для контроля всех действий, и, хотя существует возможность добавлять собственные события (UIControlEventApplicationReserver
), их количество ограничено четырьмя. Эти действия не могут передавать какую-либо информацию вместе с событием. Следовательно, они не могут быть использованы для передачи дополнительных данных.UIControl
в самом начале урока.continueTrackingWithTouch:withEvent:
, так что именно здесь нужно реализовать механизм уведомлений. Откройте CERangeSlider.m, найдите метод continueTrackingWithTouch:withEvent
и добавьте следующий код перед return YES
:[self sendActionsForControlEvents:UIControlEventValueChanged];
viewDidLoad
:[_rangeSlider addTarget:self
action:@selector(slideValueChanged:)
forControlEvents:UIControlEventValueChanged];
slideValueChanged
каждый раз, когда слайдер присылает событие UIControlEventValueChanged
.- (void)slideValueChanged:(id)control
{
NSLog(@"Slider value changed: (%.2f,%.2f)",
_rangeSlider.lowerValue, _rangeSlider.upperValue);
}
#import <QuartzCore/QuartzCore.h>
@class CERangeSlider;
@interface CERangeSliderTrackLayer : CALayer
@property (weak) CERangeSlider* slider;
@end
#import
:#import "CERangeSliderTrackLayer.h"
_trackLayer
и измените ее тип на только что созданный класс:CERangeSliderTrackLayer* _trackLayer;
initWithFrame:
и обновите код создания слоя:_trackLayer = [CERangeSliderTrackLayer layer];
_trackLayer.slider = self;
[self.layer addSublayer:_trackLayer];
_upperKnobLayer = [CERangeSliderKnobLayer layer];
_upperKnobLayer.slider = self;
[self.layer addSublayer:_upperKnobLayer];
_lowerKnobLayer = [CERangeSliderKnobLayer layer];
_lowerKnobLayer.slider = self;
[self.layer addSublayer:_lowerKnobLayer];
viewDidLoad
и сотрите ее:_rangeSlider.backgroundColor = [UIColor redColor];
@property (nonatomic) UIColor* trackColour;
@property (nonatomic) UIColor* trackHighlightColour;
@property (nonatomic) UIColor* knobColour;
@property (nonatomic) float curvaceousness;
- (float) positionForValue:(float)value;
curvaceousness
— узнаете чуть позже. И, наконец, positionForValue:
. Этот метод мы уже успели реализовать, а сейчас делаем его доступным для различных слоев.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;
#import
:#import "CERangeSlider.h"
CALayer
, который дает возможность использовать только сплошной цвет.drawInContext:
и использовать CoreGraphics API для рендеринга.Примечание: Если вы хотите узнать больше о Core Graphics, то вам рекомендован курс Core Graphics 101 tutorial series, так как детальное рассмотрение Core Graphics находится вне рамок этого урока.
@implementation
:- (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);
}
curvaceousness
, сейчас самое время попробовать!#import
:#import "CERangeSlider.h"
- (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);
}
}
- (void)setTrackColour:(UIColor *)trackColour
{
if (_trackColour != trackColour) {
_trackColour = trackColour;
[_trackLayer setNeedsDisplay];
}
}
trackColor
, этот блок кода уведомляет слой полосы слайдера о том, что ему нужно обновиться. Но, учитывая тот факт, что в API слайдера используются восемь переменных, переписывать столько раз один и тот же код — не самое лучшее решение. #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];
}
redrawLayers
вызывается для переменных, связанных с внешним видом компонента, а setLayerFrames
— для отвечающих за разметку.viewDidLoad
:[self performSelector:@selector(updateState) withObject:nil afterDelay:1.0f];
updateState
после секундной задержки. Добавим этот метод в CEViewController.m:- (void)updateState
{
_rangeSlider.trackHighlightColour = [UIColor redColor];
_rangeSlider.curvaceousness = 0.0;
}
Примечание: Код, который вы только что добавили, наглядно иллюстрирует одну из самых интересных (и, к слову, часто забываемых разработчиками) сторон разработки кастомных компонентов — их тестирование.
Когда вы разрабатываете свой элемент управления, проверить все возможные значения его свойств и их влияние на внешний вид компонента — ваша ответственность. Хорошим способом будет добавить несколько кнопок и слайдеров, каждый из которых отвечает за какое-либо свойство компонента. Таким образом можно будет тестировать элемент управления, не отвлекаясь на изменение кода.
max
, min
, upper
, lower
.upperValue
больше, чем maximumValue
? Конечно, сами вы этого никогда не сделаете — это как минимум глупо. Но нельзя гарантировать того, что кто-то другой попробует! Вам нужно удостовериться, что компонент всегда работает исправно — вне зависимости от уровня глупости разработчика.