diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 65584c5..3cefb31 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 04791FFF2ED830FA004E8522 /* KBKeyboardMaskView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */; }; 047920072ED86ABC004E8522 /* kb_guide_keyboard.gif in Resources */ = {isa = PBXBuildFile; fileRef = 047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */; }; 0479200B2ED87CEE004E8522 /* permiss_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 0479200A2ED87CEE004E8522 /* permiss_video.mp4 */; }; + 047920112ED98E7D004E8522 /* permiss_video_2.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 047920102ED98E7D004E8522 /* permiss_video_2.mp4 */; }; 047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650C2EBC8A840035E841 /* KBPanModalView.m */; }; 047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650F2EBCA8DD0035E841 /* HomeRankContentVC.m */; }; 047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C654F2EBCBA9E0035E841 /* KBShopVC.m */; }; @@ -272,6 +273,7 @@ 04791FFE2ED830FA004E8522 /* KBKeyboardMaskView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardMaskView.m; sourceTree = ""; }; 047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = kb_guide_keyboard.gif; sourceTree = ""; }; 0479200A2ED87CEE004E8522 /* permiss_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = permiss_video.mp4; sourceTree = ""; }; + 047920102ED98E7D004E8522 /* permiss_video_2.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = permiss_video_2.mp4; sourceTree = ""; }; 047C650B2EBC8A840035E841 /* KBPanModalView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPanModalView.h; sourceTree = ""; }; 047C650C2EBC8A840035E841 /* KBPanModalView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPanModalView.m; sourceTree = ""; }; 047C650E2EBCA8DD0035E841 /* HomeRankContentVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeRankContentVC.h; sourceTree = ""; }; @@ -630,6 +632,7 @@ isa = PBXGroup; children = ( 0479200A2ED87CEE004E8522 /* permiss_video.mp4 */, + 047920102ED98E7D004E8522 /* permiss_video_2.mp4 */, 047920062ED86ABC004E8522 /* kb_guide_keyboard.gif */, 04286A122ECDEBF900CE730C /* KBSkinIconMap.strings */, 04286A0E2ECDA71B00CE730C /* 001.zip */, @@ -1471,6 +1474,7 @@ 04C6EABA2EAF86530089C901 /* Assets.xcassets in Resources */, 04A9FE212EB893F10020DB6D /* Localizable.strings in Resources */, 047920072ED86ABC004E8522 /* kb_guide_keyboard.gif in Resources */, + 047920112ED98E7D004E8522 /* permiss_video_2.mp4 in Resources */, 04C6EABC2EAF86530089C901 /* LaunchScreen.storyboard in Resources */, 04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */, 04C6EABD2EAF86530089C901 /* Main.storyboard in Resources */, diff --git a/keyBoard/Class/Guard/V/KBKeyboardMaskView.h b/keyBoard/Class/Guard/V/KBKeyboardMaskView.h index 6be367d..2eecbff 100644 --- a/keyBoard/Class/Guard/V/KBKeyboardMaskView.h +++ b/keyBoard/Class/Guard/V/KBKeyboardMaskView.h @@ -6,21 +6,19 @@ // #import -#import NS_ASSUME_NONNULL_BEGIN /// 覆盖在 KBGuideVC 上方的“请选择自家键盘”蒙层 /// - 左上角:返回箭头 -/// - 中间:播放 GIF 的区域(宽=屏幕宽,高=300) +/// - 中间:播放引导视频的区域(宽=屏幕宽,高=300) /// - 点击任意空白区域:回调给外部(用于激活输入框) @interface KBKeyboardMaskView : UIView @property (nonatomic, strong, readonly) UIButton *backButton; -@property (nonatomic, strong, readonly) FLAnimatedImageView *gifView; /// 点击蒙层空白时回调(不包括 backButton) @property (nonatomic, copy) void (^tapHandler)(void); -/// 更新内部 GIF 与键盘的相对位置,保证不被遮挡 +/// 更新内部视频与键盘的相对位置,保证不被遮挡 - (void)updateForKeyboardHeight:(CGFloat)kbHeight duration:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve; diff --git a/keyBoard/Class/Guard/V/KBKeyboardMaskView.m b/keyBoard/Class/Guard/V/KBKeyboardMaskView.m index 4ec1dc9..133f17f 100644 --- a/keyBoard/Class/Guard/V/KBKeyboardMaskView.m +++ b/keyBoard/Class/Guard/V/KBKeyboardMaskView.m @@ -6,11 +6,14 @@ // #import "KBKeyboardMaskView.h" +#import @interface KBKeyboardMaskView () @property (nonatomic, strong) UIButton *backButton; -@property (nonatomic, strong) FLAnimatedImageView *gifView; -@property (nonatomic, strong) UIImageView *tipLabel; // 顶部提示图 +@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 @@ -36,16 +39,15 @@ static const CGFloat KGifViewH = (209); make.width.height.mas_equalTo(40); }]; - // GIF 区域 - _gifView = [FLAnimatedImageView new]; - _gifView.contentMode = UIViewContentModeScaleAspectFit; - _gifView.clipsToBounds = YES; - _gifView.layer.cornerRadius = 30; -// _gifView.clipsToBounds = true; - _gifView.layer.masksToBounds = true; - [self addSubview:_gifView]; + // 视频区域(尺寸与原 GIF 一致) + _videoContainerView = [UIView new]; + _videoContainerView.backgroundColor = [UIColor clearColor]; + _videoContainerView.clipsToBounds = YES; + _videoContainerView.layer.cornerRadius = 30.0; + _videoContainerView.layer.masksToBounds = YES; + [self addSubview:_videoContainerView]; - [_gifView mas_makeConstraints:^(MASConstraintMaker *make) { + [_videoContainerView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self); make.width.mas_equalTo(KBFit(316)); make.height.mas_equalTo(KBFit(KGifViewH)); @@ -62,23 +64,30 @@ static const CGFloat KGifViewH = (209); UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapMask:)]; [self addGestureRecognizer:tap]; - // 加载 GIF 资源(占位名,可按需更换为实际文件名) - NSString *gifPath = [[NSBundle mainBundle] pathForResource:@"kb_guide_keyboard" ofType:@"gif"]; - if (gifPath.length > 0) { - NSData *data = [NSData dataWithContentsOfFile:gifPath]; - if (data.length > 0) { - FLAnimatedImage *img = [FLAnimatedImage animatedImageWithGIFData:data]; - _gifView.animatedImage = img; - } - } + // 监听 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]; - // 根据键盘高度,保证 GIF 不被遮挡: + // 根据键盘高度,保证视频不被遮挡: // - 无键盘:居中显示; // - 有键盘:底部距离键盘上方 20pt,若空间不足则向上顶到顶部预留的 safe 区域。 CGFloat viewH = CGRectGetHeight(self.bounds); @@ -97,11 +106,11 @@ static const CGFloat KGifViewH = (209); if (y < topMargin) y = topMargin; } - CGRect gifFrame = self.gifView.frame; + CGRect gifFrame = self.videoContainerView.frame; gifFrame.origin.y = y; - self.gifView.frame = gifFrame; + self.videoContainerView.frame = gifFrame; - // 布局顶部提示图:底部距离 gifView 顶部 20pt,居中显示 + // 布局顶部提示图:底部距离视频容器顶部 20pt,居中显示 CGFloat labelMaxWidth = CGRectGetWidth(self.bounds) - 40.0; // 左右各留 20 if (labelMaxWidth < 0) { labelMaxWidth = 0; } UIImage *tipImage = self.tipLabel.image; @@ -117,9 +126,23 @@ static const CGFloat KGifViewH = (209); CGFloat labelH = imgH * scale; CGFloat labelX = (CGRectGetWidth(self.bounds) - labelW) * 0.5; - CGFloat labelBottom = CGRectGetMinY(self.gifView.frame) - 20.0; + 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 { @@ -137,7 +160,7 @@ static const CGFloat KGifViewH = (209); duration:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve { self.keyboardHeight = MAX(kbHeight, 0); - // 触发布局刷新,以便在 layoutSubviews 里根据最新键盘高度重算 gifView 的 Y 值 + // 触发布局刷新,以便在 layoutSubviews 里根据最新键盘高度重算视频容器的 Y 值 [self setNeedsLayout]; [UIView animateWithDuration:duration delay:0 @@ -148,4 +171,58 @@ static const CGFloat KGifViewH = (209); 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 diff --git a/keyBoard/Class/Resource/permiss_video_2.mp4 b/keyBoard/Class/Resource/permiss_video_2.mp4 new file mode 100644 index 0000000..14f9ef7 Binary files /dev/null and b/keyBoard/Class/Resource/permiss_video_2.mp4 differ