229 lines
8.2 KiB
Objective-C
229 lines
8.2 KiB
Objective-C
//
|
||
// KBKeyboardMaskView.m
|
||
// keyBoard
|
||
//
|
||
// Created by Mac on 2025/11/27.
|
||
//
|
||
|
||
#import "KBKeyboardMaskView.h"
|
||
#import <AVFoundation/AVFoundation.h>
|
||
|
||
@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
|