// // KBPermissionViewController.m // CustomKeyboard // // Created by Mac on 2025/10/27. // #import "KBPermissionViewController.h" #import static void *KBPermPlayerPresentationSizeContext = &KBPermPlayerPresentationSizeContext; @interface KBPermissionViewController () @property (nonatomic, strong) UILabel *titleLabel; // 标题 @property (nonatomic, strong) UILabel *tipsLabel; // 步骤提示 @property (nonatomic, strong) UIButton *openButton; // 去设置 @property (nonatomic, strong) UIButton *closeButton; // 去设置 @property (nonatomic, strong) UILabel *helpLabel; // 底部帮助 @property (nonatomic, strong) UIImageView *redImageView; @property (nonatomic, strong) UIImageView *bgImageView; // 权限引导视频播放器(循环播放,不提供暂停交互) @property (nonatomic, strong) AVPlayer *kb_permPlayer; @property (nonatomic, strong) AVPlayerLayer *kb_permPlayerLayer; // 承载视频的裁剪容器,高度固定为 KBFit(550),内部内容从顶部开始显示,超出的部分从底部裁剪 @property (nonatomic, strong) UIView *videoContainerView; @end @implementation KBPermissionViewController - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.bgImageView]; [self.view addSubview:self.redImageView]; // 懒加载控件 + 添加到视图 [self.view addSubview:self.closeButton]; [self.view addSubview:self.titleLabel]; [self.view addSubview:self.tipsLabel]; [self.view addSubview:self.videoContainerView]; [self.view addSubview:self.openButton]; [self.view addSubview:self.helpLabel]; // 监听 App 前后台切换,保证从设置/后台回来后视频能继续播放 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_appDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_appDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; // Masonry 约束 [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(16); make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(20); make.width.mas_equalTo(26); make.height.mas_equalTo(26); }]; [self.redImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.closeButton.mas_centerY); make.right.equalTo(self.view).offset(20); make.width.mas_equalTo(143); make.height.mas_equalTo(132); }]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.closeButton.mas_bottom).offset(13); make.left.equalTo(self.view).offset(22); make.height.mas_equalTo(24); }]; [self.tipsLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.titleLabel.mas_bottom).offset(8); make.left.equalTo(self.titleLabel); }]; // 视频容器:左右各 25,顶部距离 tipsLabel 32,高度固定为 KBFit(550) [self.videoContainerView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.tipsLabel.mas_bottom).offset(KB_DEVICE_HAS_NOTCH ? 32 : 0); make.left.equalTo(self.view).offset(25); make.right.equalTo(self.view).offset(-25); make.height.mas_equalTo(KB_DEVICE_HAS_NOTCH ? KBFit(550) : KBFit(500)); }]; [self.openButton mas_makeConstraints:^(MASConstraintMaker *make) { make.bottom.equalTo(self.view).offset(-KB_SAFE_BOTTOM-20); make.left.equalTo(self.view).offset(47); make.right.equalTo(self.view).offset(-47); make.height.mas_equalTo(60); }]; // [self.helpLabel mas_makeConstraints:^(MASConstraintMaker *make) { // make.top.equalTo(self.openButton.mas_bottom).offset(12); // make.left.equalTo(self.view).offset(24); // make.right.equalTo(self.view).offset(-24); // }]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 进入页面时自动开始播放;若当前被隐藏,则由外部显式控制 if (!self.view.hidden) { [self kb_setupPermissionVideoPlayer]; } } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // 页面离开时停止播放,避免声音继续在其他界面播放 [self.kb_permPlayer pause]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; if (!self.kb_permPlayerLayer) { return; } // 以「宽度」为基准等比缩放视频,高度按宽高比计算,从 videoContainerView 顶部对齐, // 多出的部分由容器(固定高度 KBFit(550))从底部裁剪 CGSize videoSize = self.kb_permPlayer.currentItem.presentationSize; if (videoSize.width <= 0) { return; } // 尚未拿到真实尺寸时先不布局 CGFloat aspect = videoSize.height / videoSize.width; CGRect containerBounds = self.videoContainerView.bounds; if (CGRectIsEmpty(containerBounds)) { return; } CGFloat width = CGRectGetWidth(containerBounds); // 以容器宽度为基准 CGFloat height = width * aspect; // 按宽度等比算出真实视频高度 // 顶部对齐,y = 0;若 height > 容器高度,则底部会被裁剪 self.kb_permPlayerLayer.frame = CGRectMake(0, 0, width, height); } #pragma mark - App Lifecycle - (void)kb_appDidBecomeActive:(NSNotification *)note { // 仅在当前控制器可见时恢复播放 if (self.isViewLoaded && self.view.window && !self.view.hidden) { [self kb_setupPermissionVideoPlayer]; } } - (void)kb_appDidEnterBackground:(NSNotification *)note { // 进入后台时暂停,避免资源浪费 [self.kb_permPlayer pause]; } - (void)dealloc { // 移除通知监听 [[NSNotificationCenter defaultCenter] removeObserver:self]; @try { [self.kb_permPlayer.currentItem removeObserver:self forKeyPath:@"presentationSize" context:KBPermPlayerPresentationSizeContext]; } @catch (__unused NSException *e) { // ignore if observer not set } } #pragma mark - Actions - (void)onBack { // 支持两种展示方式: // 1) 作为子控制器嵌入(无 presentingViewController)→ 交由回调让父 VC 处理(通常 pop) // 2) 作为模态弹出 → 按原策略先 pop 再 dismiss UIViewController *presenter = self.presentingViewController; if (!presenter) { if (self.backHandler) self.backHandler(); return; } UINavigationController *nav = nil; if ([presenter isKindOfClass:UINavigationController.class]) { nav = (UINavigationController *)presenter; } else if (presenter.navigationController) { nav = presenter.navigationController; } if (nav) { [nav popViewControllerAnimated:NO]; [nav dismissViewControllerAnimated:YES completion:^{ if (self.backHandler) self.backHandler(); }]; } else { [self dismissViewControllerAnimated:YES completion:^{ if (self.backHandler) self.backHandler(); }]; } } - (void)openSettings { NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; UIApplication *app = [UIApplication sharedApplication]; if ([app canOpenURL:url]) { if (@available(iOS 10.0, *)) { [app openURL:url options:@{} completionHandler:nil]; } else { [app openURL:url]; } } } - (void)closeButtonAction{ [self.navigationController popViewControllerAnimated:true]; } #pragma mark - Video Player // 初始化并开始在 cardView 中循环播放视频 - (void)kb_setupPermissionVideoPlayer { // 避免重复初始化 if (self.kb_permPlayer) { [self.kb_permPlayer play]; return; } NSURL *videoURL = [[NSBundle mainBundle] URLForResource:@"permiss_video" withExtension:@"mp4"]; if (!videoURL) { return; } AVPlayerItem *item = [AVPlayerItem playerItemWithURL:videoURL]; self.kb_permPlayer = [AVPlayer playerWithPlayerItem:item]; self.kb_permPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone; self.kb_permPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.kb_permPlayer]; // 使用等比模式,按我们计算好的 frame 显示;多余部分由容器裁剪 self.kb_permPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspect; self.kb_permPlayerLayer.cornerRadius = 20; self.kb_permPlayerLayer.masksToBounds = true; // 将视频图层添加到裁剪容器内部,由容器控制圆角与裁剪 [self.videoContainerView.layer addSublayer:self.kb_permPlayerLayer]; // 监听 presentationSize 变化,尺寸从 {0,0} 变为真实值时触发布局 [item addObserver:self forKeyPath:@"presentationSize" options:NSKeyValueObservingOptionNew context:KBPermPlayerPresentationSizeContext]; // 播放结束后从头循环 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:item]; [self.kb_permPlayer play]; } - (void)kb_resumeGuideVideoIfNeeded { // 若已初始化,则直接播放;否则按统一逻辑创建并播放 [self kb_setupPermissionVideoPlayer]; } - (void)kb_pauseGuideVideo { [self.kb_permPlayer pause]; } - (void)kb_playerItemDidReachEnd:(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_permPlayer play]; }]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == KBPermPlayerPresentationSizeContext) { // presentationSize 从 {0,0} 变为真实尺寸时,触发布局更新 dispatch_async(dispatch_get_main_queue(), ^{ [self.view setNeedsLayout]; }); return; } [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } #pragma mark - Lazy Subviews - (UIButton *)backButton { if (!_backButton) { _backButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_backButton setTitle:KBLocalized(@"common_back") forState:UIControlStateNormal]; _backButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; [_backButton setTitleColor:[UIColor darkTextColor] forState:UIControlStateNormal]; [_backButton addTarget:self action:@selector(onBack) forControlEvents:UIControlEventTouchUpInside]; } return _backButton; } - (UILabel *)titleLabel { if (!_titleLabel) { _titleLabel = [UILabel new]; _titleLabel.text = (@"key of love keyboard"); _titleLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightSemibold]; _titleLabel.textColor = [UIColor blackColor]; _titleLabel.textAlignment = NSTextAlignmentCenter; } return _titleLabel; } - (UILabel *)tipsLabel { if (!_tipsLabel) { _tipsLabel = [UILabel new]; _tipsLabel.text = (@"One-click to find a partner"); _tipsLabel.font = [UIFont systemFontOfSize:14]; _tipsLabel.textColor = [UIColor darkGrayColor]; _tipsLabel.textAlignment = NSTextAlignmentCenter; } return _tipsLabel; } - (UIButton *)openButton { if (!_openButton) { _openButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_openButton setTitle:@"Turn on the keyboard" forState:UIControlStateNormal]; _openButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; [_openButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; _openButton.backgroundColor = [UIColor colorWithHex:KBBlackValue]; _openButton.layer.cornerRadius = 30; [_openButton addTarget:self action:@selector(openSettings) forControlEvents:UIControlEventTouchUpInside]; } return _openButton; } - (UIButton *)closeButton { if (!_closeButton) { _closeButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_closeButton setImage:[UIImage imageNamed:@"close_icon"] forState:UIControlStateNormal]; [_closeButton addTarget:self action:@selector(closeButtonAction) forControlEvents:UIControlEventTouchUpInside]; } return _closeButton; } - (UILabel *)helpLabel { if (!_helpLabel) { _helpLabel = [UILabel new]; _helpLabel.text = KBLocalized(@"perm_help"); _helpLabel.font = [UIFont systemFontOfSize:12]; _helpLabel.textColor = [UIColor grayColor]; _helpLabel.textAlignment = NSTextAlignmentCenter; _helpLabel.numberOfLines = 2; } return _helpLabel; } - (UIImageView *)bgImageView{ if (!_bgImageView) { _bgImageView = [[UIImageView alloc] init]; _bgImageView.image = [UIImage imageNamed:@"qx_bg_icon"]; } return _bgImageView; } - (UIImageView *)redImageView{ if (!_redImageView) { _redImageView = [[UIImageView alloc] init]; _redImageView.image = [UIImage imageNamed:@"qx_ax_icon"]; } return _redImageView; } - (UIView *)videoContainerView { if (!_videoContainerView) { _videoContainerView = [UIView new]; _videoContainerView.backgroundColor = [UIColor clearColor]; _videoContainerView.layer.cornerRadius = 20.0; _videoContainerView.clipsToBounds = YES; // 超出容器高度的部分直接裁剪 } return _videoContainerView; } @end