// // BubbleView.m // XenonSDK // // Created by SAGESSE on 2019/5/30. // Copyright © 2019 SAGESSE. All rights reserved. // #import "BubbleView.h" #import "XenonSDK.h" @interface BubbleView () @property (nonatomic, strong) NSTimer* timer; @property (nonatomic, strong) UIPanGestureRecognizer* panGestureRecognizer; @end @implementation BubbleView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; self.contentView = [UIButton buttonWithType:UIButtonTypeSystem]; UIImage* image = [UIImage imageNamed:@"login_mine" inBundle:XenonSDK.sharedSDK.bundle compatibleWithTraitCollection:nil]; self.contentView.frame = self.bounds; self.contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self.contentView setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:UIControlStateNormal]; [self addSubview:self.contentView]; return self; } - (void)startCountdonw { // Setting timer. NSTimer* timer = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(echo:) userInfo:nil repeats:false]; [NSRunLoop.mainRunLoop addTimer:timer forMode:NSDefaultRunLoopMode]; self.timer = timer; } - (void)stopCountdown { if (self.timer == nil) { return; } [self.timer invalidate]; self.timer = nil; // Restore cursor state. self.transform = CGAffineTransformIdentity; self.contentView.hidden = NO; self.gradualView.hidden = YES; } - (CGPoint)estimateRange:(CGPoint)center inView:(UIView*)view { if (view == nil) { return center; } CGRect bounds = UIEdgeInsetsInsetRect(view.bounds, UIEdgeInsetsMake(8, 8, 8, 8));; CGFloat x = center.x; CGFloat y = center.y; // Check that the distance side is closer. if (fabs((x - self.frame.size.width / 2) - CGRectGetMinX(bounds)) < fabs((x + self.frame.size.width / 2) - CGRectGetMaxX(bounds))) { x = CGRectGetMinX(bounds) + self.frame.size.width / 2; } else { x = CGRectGetMaxX(bounds) - self.frame.size.width / 2; } // You can't go above or below the boundary. y = fmin(fmax(y, CGRectGetMinY(bounds) + self.frame.size.height / 2), CGRectGetMaxY(bounds) - self.frame.size.height / 2); return CGPointMake(x, y); } - (void)echo:(NSTimer*)timer { // Gets the window. if (self.window == nil) { return; } // Only need to create the view once. if (self.gradualView == nil) { // If no image is provided, ignore it. UIImage* image = [UIImage imageNamed:@"login_iv_hide" inBundle:XenonSDK.sharedSDK.bundle compatibleWithTraitCollection:nil]; if (image == nil) { return; } // Calculate the best display effect. CGFloat height = self.bounds.size.height; CGFloat width = height * (image.size.width / image.size.height); // Create the view and set the same default values. UIImageView* imageView = [[UIImageView alloc] initWithImage:image]; imageView.bounds = CGRectMake(0, 0, width, height); imageView.hidden = NO; imageView.userInteractionEnabled = NO; imageView.contentMode = UIViewContentModeScaleAspectFit; [self addSubview:imageView]; self.gradualView = imageView; } // Click is not allowed while hidden. self.contentView.hidden = false; self.gradualView.hidden = true; // Calculate the distance to the boundary. CGFloat offset = 0; if (fabs(CGRectGetMinX(self.frame)) < fabs(self.window.frame.size.width - CGRectGetMaxX(self.frame))) { // In left side. offset = -CGRectGetMaxX(self.frame); self.gradualView.transform = CGAffineTransformIdentity; self.gradualView.frame = CGRectMake(-CGRectGetMinX(self.frame), 0, self.gradualView.frame.size.width, self.gradualView.frame.size.height); } else { // In right side. offset = self.window.frame.size.width - CGRectGetMinX(self.frame); self.gradualView.transform = CGAffineTransformMakeScale(-1, 1); self.gradualView.frame = CGRectMake(offset - self.gradualView.frame.size.width, 0, self.gradualView.frame.size.width, self.gradualView.frame.size.height); } // Generate hidden animation. [UIView animateWithDuration:0.2 animations:^{ // step 1: hide the entire button. self.transform = CGAffineTransformMakeTranslation(offset, 0); } completion:^(BOOL finished) { // step 2: Disable click events to show hidden cursors. self.contentView.hidden = true; self.gradualView.hidden = false; // step 3: Generate display animation. [UIView animateWithDuration:0.15 animations:^{ self.transform = CGAffineTransformIdentity; }]; }]; [timer invalidate]; } - (void)move:(UIPanGestureRecognizer*)sender { CGPoint location = [sender translationInView:self.superview]; switch (sender.state) { case UIGestureRecognizerStateBegan: case UIGestureRecognizerStateChanged: case UIGestureRecognizerStatePossible: { // Begin drag event. self.transform = CGAffineTransformMakeTranslation(location.x, location.y); [self stopCountdown]; break; } case UIGestureRecognizerStateEnded: { // End drag event. self.transform = CGAffineTransformIdentity; self.center = CGPointMake(self.center.x + location.x, self.center.y + location.y); [self startCountdonw]; // Generate move animation. [UIView animateWithDuration:0.25 animations:^{ self.center = [self estimateRange:self.center inView:self.superview]; }]; // Getting the class name directly is to reduce the number of strings in the code. [XSDataCenter setValue:@[@(self.center.x), @(self.center.y)] forKey:NSStringFromClass(self.class)]; break; } default: { // End drag event in cancel. self.transform = CGAffineTransformIdentity; [self startCountdonw]; break; } } } - (void)setHidden:(BOOL)hidden { [super setHidden:hidden]; if (self.timer == nil || !self.gradualView.hidden) { return; } // If you are hiding, countdown is not allowed. if (hidden) { // Cancel countdown. [self.timer invalidate]; } else { // Restore countdown. [self startCountdonw]; } } - (void)setCenter:(CGPoint)center { [super setCenter:center]; if (self.superview == nil) { return; } if (CGRectGetMinX(self.frame) < CGRectGetWidth(self.superview.bounds) / 2) { // In left side. self.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin; } else { // In right side. self.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin; } } - (void)willMoveToSuperview:(UIView *)newSuperview { // Remove or Add? if (newSuperview == nil) { // When superview remove, the pan gesture recognizer need remove. [self.panGestureRecognizer.view removeGestureRecognizer:self.panGestureRecognizer]; self.panGestureRecognizer = nil; [self stopCountdown]; return; } if (self.panGestureRecognizer != nil) { return ; } CGPoint center = CGPointMake(newSuperview.bounds.size.width, newSuperview.bounds.size.height / 2); // Getting the class name directly is to reduce the number of strings in the code. NSArray* data = [XSDataCenter valueForKey:NSStringFromClass(self.class)]; if (data.count == 2) { center.x = [data[0] doubleValue]; center.y = [data[1] doubleValue]; } UIPanGestureRecognizer* panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(move:)]; panGestureRecognizer.delegate = self; panGestureRecognizer.delaysTouchesBegan = false; panGestureRecognizer.delaysTouchesEnded = true; panGestureRecognizer.cancelsTouchesInView = true; [newSuperview addGestureRecognizer:panGestureRecognizer]; self.center = [self estimateRange:center inView:newSuperview]; self.panGestureRecognizer = panGestureRecognizer; [self startCountdonw]; } - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (self.hidden || !self.userInteractionEnabled) { return nil; } if (CGRectContainsPoint(self.contentView.frame, point) && !self.contentView.hidden) { return self.contentView; } if (self.gradualView != nil && CGRectContainsPoint(self.gradualView.frame, point) && !self.gradualView.hidden) { return self.gradualView; } return nil; } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if (self.hidden) { return NO; } CGRect bounds = UIEdgeInsetsInsetRect(self.bounds, UIEdgeInsetsMake(-20, -20, -20, -20)); return CGRectContainsPoint(bounds, [gestureRecognizer locationInView:self]); } @end