Иногда случается так, что нужно втиснуть в уже готовое приложение одиночный UITableView или какую-нибудь часто используемую форму. Выделять для этого целый View Controller не всегда оправдано, и поэтому однажды мне пришлось сделать вот такой вот элемент интерфейса:
Ничего сложного в нём нет: пара UIView и UIPanGestureRecognizer. Хотите узнать, как сделать что-нибудь подобное?
DKNotificationView
Самоё легкое решение для подобных элементов интерфейса — это класс, унаследованный от UIView, который мы потом сможем перетаскивать по экрану с помощью UIPanGestureRecognizer. Ну так вот, интерфейс этого класса я реализовал так:
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
@interface DKNotificationView : UIView
typedef enum {
DKNotificationViewStateActive = 1,
DKNotificationViewStateMoving,
DKNotificationViewStateResting
} DKNotificationViewState;
@property CGPoint startPoint;
@property CGRect restingFrame;
@property CGRect activeFrame;
@property (nonatomic) DKNotificationViewState state;
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) UIView *contentView;
- (id)initWithFrame:(CGRect)frame inView:(UIView *)view;
- (void)setState:(DKNotificationViewState)state;
@end
Реализация
Ну а теперь, увидев интерфейс класса, перейдём к реализации. Основная идея: наш Notification View должен красиво появляться снизу экрана (муза — Notification Center) и может содержать произвольный контент. Рассмотрим поближе методы, реализованные в .m-файле.
Инициализация
При реализации нам нужно запомнить сразу два CGRect, которые будут использоваться в активном и неактивном состояниях. Поскольку более понятным будет указание «активного» CGRect при инициализации, «неактивный» будем вычислять.
- (id)initWithFrame:(CGRect)frame inView:(UIView *)view
{
self = [super initWithFrame:frame];
if (self) {
self.activeFrame = frame;
self.restingFrame = CGRectMake(_activeFrame.origin.x,
_activeFrame.origin.y + _activeFrame.size.height - 20,
_activeFrame.size.width,
_activeFrame.size.height);
self.frame = self.restingFrame;
self.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
self.contentView = [[UIView alloc] initWithFrame:CGRectMake(0,
20,
self.frame.size.width,
self.frame.size.height - 20)];
self.contentView.backgroundColor = [UIColor clearColor];
self.state = DKNotificationViewStateResting;
UIPanGestureRecognizer *gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self
action:@selector(panGesture:)];
[self addGestureRecognizer:gestureRecognizer];
[self addObserver:self
forKeyPath:@"frame"
options:NSKeyValueObservingOptionOld
context:NULL];
[view addSubview:self];
}
return self;
}
Видите observer? Он нам понадобится в дальнейшем, ведь мы же хотим красивый интерфейс.
Переключение состояний и перемещение
У моего класса всего три состояния:
- DKNotificationViewActive — активное, когда contentView виден и непрозрачен;
- DKNotificationViewMoving — полуактивное, когда contentView виден и имеет изменяющуюся прозрачность;
- DKNotificationViewResting — неактивное, когда contentView не виден.
Вы, конечно же, можете таких состояний сделать и меньше, и больше. Но перейдём к тому, как можно реализовать переключение этих состояний:
- (void)panGesture:(UIPanGestureRecognizer *)sender {
CGPoint translatedPoint;
[[self layer] removeAllAnimations];
switch ([sender state]) {
case UIGestureRecognizerStateBegan:
_startPoint = self.frame.origin;
self.superview.userInteractionEnabled = NO;
self.state = DKNotificationViewStateMoving;
break;
case UIGestureRecognizerStateChanged:
translatedPoint = [sender translationInView:self.superview];
translatedPoint = CGPointMake(0, _startPoint.y + translatedPoint.y);
if ((translatedPoint.y >= self.activeFrame.origin.y) &
(translatedPoint.y <= self.restingFrame.origin.y)) {
self.frame = CGRectMake(self.frame.origin.x,
translatedPoint.y,
self.frame.size.width,
self.frame.size.height);
}
break;
case UIGestureRecognizerStateEnded:
if ([sender velocityInView:self.superview].y < 0) {
if (self.frame.origin.y < self.restingFrame.origin.y - 2)
self.state = DKNotificationViewStateActive;
else
self.state = DKNotificationViewStateResting;
}
else {
if (self.frame.origin.y > self.activeFrame.origin.y + 2)
self.state = DKNotificationViewStateResting;
else
self.state = DKNotificationViewStateActive;
}
self.superview.userInteractionEnabled = YES;
break;
default:
break;
}
}
Это метод, используемый при перетаскивании нашего View. Основную часть этого метода занимает switch состояний UIPanGestureRecognizer'а: UIGestureRecognizerStateBegan, UIGestureRecognizerStateChanged и UIGestureRecognizerStateEnded.
Как видно из кода, в первом случае мы лишь переключаем состояние нашего контрола, во втором — обрабатываем перемещение вслед за пальцем с помощью [sender translationInView:self.superview], а в третьем определяем, в какое же состояние должен перейти контрол после завершения перетаскивания, за что отвечает [sender velocityInView:self.superview]. Подробнее об этом можно почитать в документации.
Теперь нужно определиться с переключением состояний нашего контрола, тех, которые DKNotificationViewState. Я подумал, что при переключении состояний можно изменять прозрачность contentView, а в «неактивном» состоянии contentView можно вообще удалять с контрола.
- (void)setState:(DKNotificationViewState)state {
_state = state;
CGRect finalFrame;
switch (state) {
case DKNotificationViewStateMoving:
[self.timer invalidate];
self.timer = nil;
if (![self.subviews containsObject:self.contentView]) {
[self addSubview:self.contentView];
}
return;
case DKNotificationViewStateResting:
finalFrame = self.restingFrame;
[self.timer invalidate];
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(hideContentView)
userInfo:nil
repeats:NO];
break;
case DKNotificationViewStateActive:
[self.timer invalidate];
self.timer = nil;
finalFrame = self.activeFrame;
if (![self.subviews containsObject:self.contentView]) {
[self addSubview:self.contentView];
}
break;
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.4];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
[self setFrame:finalFrame];
[UIView commitAnimations];
}
При переключении состояний скрывается и восстанавливается contentView и анимированно меняется frame нашего контрола. Есть два нюанса: для скрытия contentView используется таймер (вдруг пользователь захочет поиграться и будет дёргать туда-сюда), и в состоянии DKNotificationViewMoving анимации не используются — мы просто заканчиваем return'ом.
Если вы не представляете, как изменять прозрачность contentView'а при перетаскивании — ничего страшного. Это легко сделать через тот самый observer, наблюдая за атрибутом frame нашего контрола.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if([keyPath isEqualToString:@"frame"]) {
self.contentView.alpha = (-self.frame.origin.y + self.activeFrame.origin.y) / (self.activeFrame.origin.y + self.activeFrame.size.height - 20) + 1;
}
}
Всё-таки математика — царица наук, пусть даже на таком уровне. Вы только потом не забудьте удалить этот observer в dealloc, даже если используете ARC.
Ну вот, на этом, пожалуй, всё. Хотя нет, есть ещё одно пожелание: рисуйте фоновые картиночки и градиенты в drawRect: — это позволит улучшить производительность, поскольку системе не придётся работать с полупрозрачными слоями. Удачных вам начинаний, уважаемые начинающие NS-кодеры. :)
P.S. Если кому-то нужно, даю ссылку на рипозиторий на GitHub.
Автор: PATOGEN