// // AVSettingsView.m // AVDemo // #import "AVSettingsView.h" #import "AVConstants.h" @interface AVSettingsView () @property (nonatomic, copy) AVSettingsViewCallback callback; @property (nonatomic, strong) AVVideoConfiguration *tempConfig; @property (nonatomic, assign) AVSettingsFieldMask fieldsMask; @property (nonatomic, strong) UIView *containerView; @property (nonatomic, strong) UIScrollView *scrollView; @property (nonatomic, strong) UIView *contentView; @property (nonatomic, strong) UITextField *streamIdField; @property (nonatomic, strong) UITextField *nicknameField; @property (nonatomic, strong) UISegmentedControl *codecSegment; @property (nonatomic, strong) UISegmentedControl *resolutionSegment; @property (nonatomic, strong) UITextField *fpsField; @property (nonatomic, strong) UITextField *maxBitrateField; @property (nonatomic, strong) UITextField *minBitrateField; @property (nonatomic, strong) NSLayoutConstraint *containerCenterYConstraint; @property (nonatomic, weak) UITextField *activeTextField; // 当前激活的输入框 @end @implementation AVSettingsView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // UI will be setup when showing // 监听键盘通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)setupUI { self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5]; // 在方法开始时统一计算屏幕尺寸和方向,避免重复定义 CGFloat screenHeight = CGRectGetHeight([UIScreen mainScreen].bounds); CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds); BOOL isLandscape = screenWidth > screenHeight; _containerView = [[UIView alloc] init]; _containerView.backgroundColor = [UIColor systemBackgroundColor]; _containerView.layer.cornerRadius = 12; _containerView.layer.masksToBounds = YES; [self addSubview:_containerView]; _containerView.translatesAutoresizingMaskIntoConstraints = NO; _containerCenterYConstraint = [_containerView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor]; // 根据屏幕方向动态调整容器高度 CGFloat maxHeight = isLandscape ? (screenHeight * 0.85) : 600; // 横屏时使用屏幕高度的85% [NSLayoutConstraint activateConstraints:@[ [_containerView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:20], [_containerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-20], _containerCenterYConstraint, [_containerView.heightAnchor constraintLessThanOrEqualToConstant:maxHeight] ]]; // Add dismiss button UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeSystem]; [dismissButton setTitle:@"×" forState:UIControlStateNormal]; dismissButton.titleLabel.font = [UIFont systemFontOfSize:30]; [dismissButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; [_containerView addSubview:dismissButton]; dismissButton.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [dismissButton.topAnchor constraintEqualToAnchor:_containerView.topAnchor constant:10], [dismissButton.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor constant:-10], [dismissButton.widthAnchor constraintEqualToConstant:44], [dismissButton.heightAnchor constraintEqualToConstant:44] ]]; // Title UILabel *titleLabel = [[UILabel alloc] init]; titleLabel.text = @"设置"; titleLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightBold]; [_containerView addSubview:titleLabel]; titleLabel.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [titleLabel.topAnchor constraintEqualToAnchor:_containerView.topAnchor constant:20], [titleLabel.centerXAnchor constraintEqualToAnchor:_containerView.centerXAnchor] ]]; UIScrollView *scrollView = [[UIScrollView alloc] init]; scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; // 允许滑动关闭键盘 [_containerView addSubview:scrollView]; _scrollView = scrollView; // 动态调整 scrollView 和按钮之间的间距 CGFloat buttonAreaHeight = isLandscape ? 64 : 80; // 横屏时减小按钮区域高度 scrollView.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [scrollView.topAnchor constraintEqualToAnchor:titleLabel.bottomAnchor constant:20], [scrollView.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor], [scrollView.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor], [scrollView.bottomAnchor constraintEqualToAnchor:_containerView.bottomAnchor constant:-buttonAreaHeight] ]]; UIView *contentView = [[UIView alloc] init]; [scrollView addSubview:contentView]; _contentView = contentView; // 保存引用 contentView.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [contentView.topAnchor constraintEqualToAnchor:scrollView.topAnchor], [contentView.leadingAnchor constraintEqualToAnchor:scrollView.leadingAnchor], [contentView.trailingAnchor constraintEqualToAnchor:scrollView.trailingAnchor], [contentView.bottomAnchor constraintEqualToAnchor:scrollView.bottomAnchor], [contentView.widthAnchor constraintEqualToAnchor:scrollView.widthAnchor] ]]; // Add form elements (similar to settings VC) CGFloat topOffset = 10; CGFloat spacing = 20; // Stream ID (conditional) if (self.fieldsMask & AVSettingsFieldStreamId) { UILabel *streamIdLabel = [[UILabel alloc] init]; streamIdLabel.text = @"Stream ID"; streamIdLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; [contentView addSubview:streamIdLabel]; streamIdLabel.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [streamIdLabel.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [streamIdLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset] ]]; topOffset += 20; _streamIdField = [[UITextField alloc] init]; _streamIdField.placeholder = @"请输入Stream ID"; _streamIdField.borderStyle = UITextBorderStyleRoundedRect; _streamIdField.delegate = self; _streamIdField.returnKeyType = UIReturnKeyDone; [contentView addSubview:_streamIdField]; _streamIdField.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [_streamIdField.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [_streamIdField.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:-20], [_streamIdField.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset], [_streamIdField.heightAnchor constraintEqualToConstant:40] ]]; topOffset += 40 + spacing; } // Nickname (conditional) if (self.fieldsMask & AVSettingsFieldNickname) { UILabel *nicknameLabel = [[UILabel alloc] init]; nicknameLabel.text = @"用户昵称"; nicknameLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; [contentView addSubview:nicknameLabel]; nicknameLabel.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [nicknameLabel.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [nicknameLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset] ]]; topOffset += 20; _nicknameField = [[UITextField alloc] init]; _nicknameField.placeholder = @"请输入昵称"; _nicknameField.borderStyle = UITextBorderStyleRoundedRect; _nicknameField.delegate = self; _nicknameField.returnKeyType = UIReturnKeyDone; [contentView addSubview:_nicknameField]; _nicknameField.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [_nicknameField.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [_nicknameField.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:-20], [_nicknameField.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset], [_nicknameField.heightAnchor constraintEqualToConstant:40] ]]; topOffset += 40 + spacing; } // Video parameters (conditional) if (self.fieldsMask & AVSettingsFieldVideoParams) { // Resolution UILabel *resolutionLabel = [[UILabel alloc] init]; resolutionLabel.text = @"分辨率"; resolutionLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; [contentView addSubview:resolutionLabel]; resolutionLabel.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [resolutionLabel.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [resolutionLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset] ]]; topOffset += 20; _resolutionSegment = [[UISegmentedControl alloc] initWithItems:@[@"360p", @"480p", @"540p", @"720p"]]; [contentView addSubview:_resolutionSegment]; _resolutionSegment.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [_resolutionSegment.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [_resolutionSegment.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:-20], [_resolutionSegment.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset], [_resolutionSegment.heightAnchor constraintEqualToConstant:32] ]]; topOffset += 32 + spacing; // FPS UILabel *fpsLabel = [[UILabel alloc] init]; fpsLabel.text = @"帧率 (FPS)"; fpsLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; [contentView addSubview:fpsLabel]; fpsLabel.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [fpsLabel.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [fpsLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset] ]]; topOffset += 20; _fpsField = [[UITextField alloc] init]; _fpsField.placeholder = @"例如: 30"; _fpsField.borderStyle = UITextBorderStyleRoundedRect; _fpsField.keyboardType = UIKeyboardTypeNumberPad; _fpsField.delegate = self; [self addDoneToolbarToTextField:_fpsField]; [contentView addSubview:_fpsField]; _fpsField.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [_fpsField.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [_fpsField.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:-20], [_fpsField.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset], [_fpsField.heightAnchor constraintEqualToConstant:40] ]]; topOffset += 40 + spacing; // Max Bitrate UILabel *maxBitrateLabel = [[UILabel alloc] init]; maxBitrateLabel.text = @"最大码率 (kbps)"; maxBitrateLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; [contentView addSubview:maxBitrateLabel]; maxBitrateLabel.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [maxBitrateLabel.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [maxBitrateLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset] ]]; topOffset += 20; _maxBitrateField = [[UITextField alloc] init]; _maxBitrateField.placeholder = @"例如: 2000"; _maxBitrateField.borderStyle = UITextBorderStyleRoundedRect; _maxBitrateField.keyboardType = UIKeyboardTypeNumberPad; _maxBitrateField.delegate = self; [self addDoneToolbarToTextField:_maxBitrateField]; [contentView addSubview:_maxBitrateField]; _maxBitrateField.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [_maxBitrateField.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [_maxBitrateField.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:-20], [_maxBitrateField.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset], [_maxBitrateField.heightAnchor constraintEqualToConstant:40] ]]; topOffset += 40 + spacing; // Min Bitrate UILabel *minBitrateLabel = [[UILabel alloc] init]; minBitrateLabel.text = @"最小码率 (kbps)"; minBitrateLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; [contentView addSubview:minBitrateLabel]; minBitrateLabel.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [minBitrateLabel.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [minBitrateLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset] ]]; topOffset += 20; _minBitrateField = [[UITextField alloc] init]; _minBitrateField.placeholder = @"例如: 500"; _minBitrateField.borderStyle = UITextBorderStyleRoundedRect; _minBitrateField.keyboardType = UIKeyboardTypeNumberPad; _minBitrateField.delegate = self; [self addDoneToolbarToTextField:_minBitrateField]; [contentView addSubview:_minBitrateField]; _minBitrateField.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [_minBitrateField.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20], [_minBitrateField.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:-20], [_minBitrateField.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset], [_minBitrateField.heightAnchor constraintEqualToConstant:40] ]]; topOffset += 40 + spacing; } // End of video parameters conditional block [NSLayoutConstraint activateConstraints:@[ [contentView.heightAnchor constraintEqualToConstant:topOffset] ]]; // Buttons UIButton *applyButton = [UIButton buttonWithType:UIButtonTypeSystem]; [applyButton setTitle:@"应用" forState:UIControlStateNormal]; applyButton.backgroundColor = [UIColor systemBlueColor]; [applyButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; applyButton.layer.cornerRadius = 8; [applyButton addTarget:self action:@selector(applyTapped) forControlEvents:UIControlEventTouchUpInside]; [_containerView addSubview:applyButton]; // 根据屏幕方向动态调整按钮高度和底部间距 CGFloat buttonHeight = isLandscape ? 36 : 44; // 横屏时按钮更矮 CGFloat buttonBottomMargin = isLandscape ? 12 : 20; // 横屏时减小底部边距 applyButton.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [applyButton.leadingAnchor constraintEqualToAnchor:_containerView.leadingAnchor constant:20], [applyButton.trailingAnchor constraintEqualToAnchor:_containerView.trailingAnchor constant:-20], [applyButton.bottomAnchor constraintEqualToAnchor:_containerView.bottomAnchor constant:-buttonBottomMargin], [applyButton.heightAnchor constraintEqualToConstant:buttonHeight] ]]; // Tap to dismiss UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(backgroundTapped:)]; [self addGestureRecognizer:tap]; } - (void)showInViewController:(UIViewController *)viewController withConfig:(AVVideoConfiguration *)config fieldsMask:(AVSettingsFieldMask)fieldsMask callback:(AVSettingsViewCallback)callback { self.callback = callback; self.tempConfig = [config copy]; self.fieldsMask = fieldsMask; // Setup UI now that we have fieldsMask [self setupUI]; // Load config values if (_streamIdField) { // Generate random streamId if empty or default if (config.streamId.length == 0 || [config.streamId isEqualToString:@"stream"]) { // Generate random 3-digit number (100-999) NSInteger randomNumber = 100 + arc4random_uniform(900); _streamIdField.text = [NSString stringWithFormat:@"stream%ld", (long)randomNumber]; // Update the temp config with the generated streamId self.tempConfig.streamId = _streamIdField.text; } else { _streamIdField.text = config.streamId; } } if (_nicknameField) { _nicknameField.text = config.nickname; } if (_resolutionSegment) { _resolutionSegment.selectedSegmentIndex = [config currentResolution]; } if (_fpsField) { _fpsField.text = [NSString stringWithFormat:@"%ld", (long)config.videoFrameRate]; } if (_maxBitrateField) { _maxBitrateField.text = [NSString stringWithFormat:@"%ld", (long)(config.videoBitRate / 1000)]; } if (_minBitrateField) { _minBitrateField.text = [NSString stringWithFormat:@"%ld", (long)(config.videoMinBitRate / 1000)]; } self.frame = viewController.view.bounds; self.alpha = 0; [viewController.view addSubview:self]; [UIView animateWithDuration:0.3 animations:^{ self.alpha = 1.0; }]; } - (void)dismiss { [UIView animateWithDuration:0.3 animations:^{ self.alpha = 0; } completion:^(BOOL finished) { [self removeFromSuperview]; }]; } - (void)applyTapped { // Update config with all field values if (_streamIdField) { self.tempConfig.streamId = _streamIdField.text.length > 0 ? _streamIdField.text : @""; } if (_nicknameField) { self.tempConfig.nickname = _nicknameField.text.length > 0 ? _nicknameField.text : @"test"; } if (_resolutionSegment) { [self.tempConfig setResolution:_resolutionSegment.selectedSegmentIndex]; } if (_fpsField) { NSInteger fps = [_fpsField.text integerValue]; // Validate FPS if (fps <= 0) { fps = 30; } self.tempConfig.videoFrameRate = fps; self.tempConfig.videoMaxKeyframeInterval = fps * 2; } if (_maxBitrateField) { NSInteger maxBitrate = [_maxBitrateField.text integerValue]; // Validate max bitrate if (maxBitrate <= 0) { maxBitrate = 2000; } self.tempConfig.videoBitRate = maxBitrate * 1000; // Convert kbps to bps } if (_minBitrateField) { NSInteger minBitrate = [_minBitrateField.text integerValue]; // Validate min bitrate if (minBitrate <= 0) { minBitrate = 500; } self.tempConfig.videoMinBitRate = minBitrate * 1000; // Convert kbps to bps } if (self.callback) { self.callback(self.tempConfig); } [self dismiss]; } - (void)backgroundTapped:(UITapGestureRecognizer *)recognizer { CGPoint location = [recognizer locationInView:self]; if (!CGRectContainsPoint(_containerView.frame, location)) { // 点击背景关闭弹窗 [self dismiss]; } else { // 点击容器内部,收起键盘 [self endEditing:YES]; } } #pragma mark - Keyboard Handling - (void)keyboardWillShow:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGFloat keyboardHeight = keyboardFrame.size.height; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; // 方案:调整 ScrollView 的 contentInset,而不是移动整个容器 // 计算键盘遮挡的高度 CGRect containerFrameInWindow = [_containerView convertRect:_containerView.bounds toView:self]; CGFloat containerBottom = CGRectGetMaxY(containerFrameInWindow); CGFloat keyboardTop = CGRectGetHeight(self.bounds) - keyboardHeight; CGFloat overlap = containerBottom - keyboardTop; if (overlap > 0) { // 键盘会遮挡容器,调整 scrollView 的 contentInset UIEdgeInsets contentInset = _scrollView.contentInset; contentInset.bottom = overlap + 20; // 额外留20px边距 [UIView animateWithDuration:duration delay:0 options:(curve << 16) animations:^{ self.scrollView.contentInset = contentInset; self.scrollView.scrollIndicatorInsets = contentInset; // 如果有激活的输入框,滚动到可见位置 if (self.activeTextField) { [self scrollToTextField:self.activeTextField]; } } completion:nil]; } } - (void)keyboardWillHide:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; // 恢复 scrollView 的 contentInset [UIView animateWithDuration:duration delay:0 options:(curve << 16) animations:^{ self.scrollView.contentInset = UIEdgeInsetsZero; self.scrollView.scrollIndicatorInsets = UIEdgeInsetsZero; } completion:nil]; } - (void)scrollToTextField:(UITextField *)textField { // 计算 textField 在 scrollView 中的位置 CGRect textFieldFrame = [textField convertRect:textField.bounds toView:_contentView]; // 添加一些边距,确保输入框上下都有空间 CGFloat padding = 20; CGRect targetRect = CGRectMake(textFieldFrame.origin.x, textFieldFrame.origin.y - padding, textFieldFrame.size.width, textFieldFrame.size.height + padding * 2); // 滚动到目标位置 [_scrollView scrollRectToVisible:targetRect animated:YES]; } #pragma mark - Helper Methods - (void)addDoneToolbarToTextField:(UITextField *)textField { UIToolbar *toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.bounds), 44)]; toolbar.barStyle = UIBarStyleDefault; UIBarButtonItem *flexSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(doneButtonTapped)]; toolbar.items = @[flexSpace, doneButton]; textField.inputAccessoryView = toolbar; } - (void)doneButtonTapped { [self endEditing:YES]; } #pragma mark - UITextFieldDelegate - (void)textFieldDidBeginEditing:(UITextField *)textField { self.activeTextField = textField; // 延迟一点点,等键盘动画开始 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self scrollToTextField:textField]; }); } - (void)textFieldDidEndEditing:(UITextField *)textField { if (self.activeTextField == textField) { self.activeTextField = nil; } } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } @end