// // KBKeyboardMaskView.m // keyBoard // // Created by Mac on 2025/11/27. // #import "KBKeyboardMaskView.h" #import @interface KBKeyboardMaskView () @property (nonatomic, strong) UIButton *backButton; @property (nonatomic, strong) UIView *videoContainerView; // 承载引导视频的容器,复用原 GIF 尺寸逻辑 @property (nonatomic, strong) AVPlayer *kb_maskPlayer; // 键盘蒙层引导视频播放器 @property (nonatomic, strong) AVPlayerLayer *kb_maskPlayerLayer; @property (nonatomic, strong) UIImageView *tipLabel; // 顶部提示图 @property (nonatomic, assign) CGFloat keyboardHeight; @end @implementation KBKeyboardMaskView static const CGFloat KGifViewH = (209); - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (!self) return nil; self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5]; self.userInteractionEnabled = YES; // 返回按钮 _backButton = [UIButton buttonWithType:UIButtonTypeCustom]; UIImage *backImg = [UIImage imageNamed:@"close_white2_icon"]; [_backButton setImage:backImg forState:UIControlStateNormal]; [self addSubview:_backButton]; [_backButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self).offset(9); make.top.equalTo(self.mas_safeAreaLayoutGuideTop).offset(4); make.width.height.mas_equalTo(40); }]; // 视频区域(尺寸与原 GIF 一致) _videoContainerView = [UIView new]; _videoContainerView.backgroundColor = [UIColor clearColor]; _videoContainerView.clipsToBounds = YES; _videoContainerView.layer.cornerRadius = 30.0; _videoContainerView.layer.masksToBounds = YES; [self addSubview:_videoContainerView]; [_videoContainerView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self); make.width.mas_equalTo(KBFit(316)); make.height.mas_equalTo(KBFit(KGifViewH)); // 竖直方向不在这里约束,由 layoutSubviews 手动布局 }]; // 顶部提示图 _tipLabel = [UIImageView new]; _tipLabel.image = [UIImage imageNamed:@"mask_top_title"]; _tipLabel.contentMode = UIViewContentModeScaleAspectFit; [self addSubview:_tipLabel]; // 整个蒙层点击:激活输入框 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapMask:)]; [self addGestureRecognizer:tap]; // 监听 App 前后台,保证从后台返回时视频能继续播放 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_maskAppDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_maskAppDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; // 初始化并开始播放引导视频(循环) [self kb_setupGuideVideoPlayerIfNeeded]; return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)layoutSubviews { [super layoutSubviews]; // 根据键盘高度,保证视频不被遮挡: // - 无键盘:居中显示; // - 有键盘:底部距离键盘上方 20pt,若空间不足则向上顶到顶部预留的 safe 区域。 CGFloat viewH = CGRectGetHeight(self.bounds); CGFloat gifH = KBFit(KGifViewH); CGFloat topMargin = 80.0; // 预留给返回按钮和标题等 CGFloat bottomMargin = 20.0; CGFloat y = 0; if (self.keyboardHeight <= 0) { // 无键盘:垂直居中 y = (viewH - gifH) * 0.5; if (y < topMargin) y = topMargin; } else { CGFloat maxBottom = viewH - self.keyboardHeight - bottomMargin; y = maxBottom - gifH; if (y < topMargin) y = topMargin; } CGRect gifFrame = self.videoContainerView.frame; gifFrame.origin.y = y; self.videoContainerView.frame = gifFrame; // 布局顶部提示图:底部距离视频容器顶部 20pt,居中显示 CGFloat labelMaxWidth = CGRectGetWidth(self.bounds) - 40.0; // 左右各留 20 if (labelMaxWidth < 0) { labelMaxWidth = 0; } UIImage *tipImage = self.tipLabel.image; if (!tipImage) { return; } CGFloat imgW = tipImage.size.width; CGFloat imgH = tipImage.size.height; if (imgW <= 0 || imgH <= 0) { return; } CGFloat scale = 1.0; if (imgW > labelMaxWidth && labelMaxWidth > 0) { scale = labelMaxWidth / imgW; } CGFloat labelW = imgW * scale; CGFloat labelH = imgH * scale; CGFloat labelX = (CGRectGetWidth(self.bounds) - labelW) * 0.5; CGFloat labelBottom = CGRectGetMinY(self.videoContainerView.frame) - 20.0; CGFloat labelY = labelBottom - labelH; self.tipLabel.frame = CGRectMake(labelX, labelY, labelW, labelH); // 使播放器图层铺满容器 if (self.kb_maskPlayerLayer) { self.kb_maskPlayerLayer.frame = self.videoContainerView.bounds; } } - (void)setHidden:(BOOL)hidden { [super setHidden:hidden]; if (hidden) { [self.kb_maskPlayer pause]; } else { [self.kb_maskPlayer play]; } } - (void)onTapMask:(UITapGestureRecognizer *)gr { CGPoint p = [gr locationInView:self]; // 如果点在返回按钮区域内,则不走 tapHandler(交由按钮自己的事件处理) if (CGRectContainsPoint(self.backButton.frame, p)) { return; } if (self.tapHandler) { self.tapHandler(); } } - (void)updateForKeyboardHeight:(CGFloat)kbHeight duration:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve { self.keyboardHeight = MAX(kbHeight, 0); // 触发布局刷新,以便在 layoutSubviews 里根据最新键盘高度重算视频容器的 Y 值 [self setNeedsLayout]; [UIView animateWithDuration:duration delay:0 options:curve animations:^{ [self layoutIfNeeded]; } completion:nil]; } #pragma mark - App Lifecycle - (void)kb_maskAppDidBecomeActive:(NSNotification *)note { // 仅在当前蒙层真正显示在窗口上时恢复播放 if (self.window && !self.hidden) { if (!self.kb_maskPlayer) { [self kb_setupGuideVideoPlayerIfNeeded]; } else { [self.kb_maskPlayer play]; } } } - (void)kb_maskAppDidEnterBackground:(NSNotification *)note { [self.kb_maskPlayer pause]; } #pragma mark - Video - (void)kb_setupGuideVideoPlayerIfNeeded { if (self.kb_maskPlayer) { return; } NSURL *videoURL = [[NSBundle mainBundle] URLForResource:@"permiss_video_2" withExtension:@"mp4"]; if (!videoURL) { return; } AVPlayerItem *item = [AVPlayerItem playerItemWithURL:videoURL]; self.kb_maskPlayer = [AVPlayer playerWithPlayerItem:item]; self.kb_maskPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone; self.kb_maskPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.kb_maskPlayer]; self.kb_maskPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspect; self.kb_maskPlayerLayer.cornerRadius = 30.0; self.kb_maskPlayerLayer.masksToBounds = YES; [self.videoContainerView.layer addSublayer:self.kb_maskPlayerLayer]; // 循环播放 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_maskPlayerDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:item]; [self.kb_maskPlayer play]; } - (void)kb_maskPlayerDidReachEnd:(NSNotification *)note { AVPlayerItem *item = (AVPlayerItem *)note.object; if (!item) return; __weak typeof(self) weakSelf = self; [item seekToTime:kCMTimeZero completionHandler:^(BOOL finished) { __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf.kb_maskPlayer play]; }]; } @end