Красивый Notification View своими руками

в 19:27, , рубрики: cocoa touch, ios development, uikit, Программирование, разработка под iOS, метки: , ,

Иногда случается так, что нужно втиснуть в уже готовое приложение одиночный UITableView или какую-нибудь часто используемую форму. Выделять для этого целый View Controller не всегда оправдано, и поэтому однажды мне пришлось сделать вот такой вот элемент интерфейса:

Красивый Notification View своими руками

Ничего сложного в нём нет: пара 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 можно вообще удалять с контрола.

Красивый Notification View своими руками

- (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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js