Files
SellyCloudSDK_demo/Example/SellyCloudSDK/Views/AVSettingsView.m
2026-04-07 18:20:16 +08:00

600 lines
27 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// AVSettingsView.m
// AVDemo
//
#import "AVSettingsView.h"
#import "AVConstants.h"
@interface AVSettingsView () <UITextFieldDelegate>
@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) UITextField *xorKeyField;
@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;
}
// XOR Key (after nickname, before video params)
if (self.fieldsMask & AVSettingsFieldXorKey) {
UILabel *xorKeyLabel = [[UILabel alloc] init];
xorKeyLabel.text = @"加密密钥 (Hex)";
xorKeyLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
[contentView addSubview:xorKeyLabel];
xorKeyLabel.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[xorKeyLabel.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20],
[xorKeyLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset]
]];
topOffset += 20;
_xorKeyField = [[UITextField alloc] init];
_xorKeyField.placeholder = @"如 AABBCCDD留空不加密";
_xorKeyField.borderStyle = UITextBorderStyleRoundedRect;
_xorKeyField.autocapitalizationType = UITextAutocapitalizationTypeNone;
_xorKeyField.autocorrectionType = UITextAutocorrectionTypeNo;
_xorKeyField.delegate = self;
_xorKeyField.returnKeyType = UIReturnKeyDone;
[contentView addSubview:_xorKeyField];
_xorKeyField.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[_xorKeyField.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20],
[_xorKeyField.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:-20],
[_xorKeyField.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset],
[_xorKeyField.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)];
}
if (_xorKeyField) {
// Load from config first, fallback to cached value
NSString *key = config.xorKey;
if (key.length == 0) {
key = [[NSUserDefaults standardUserDefaults] stringForKey:@"selly_xor_key_cache"];
}
_xorKeyField.text = key ?: @"";
}
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 (_xorKeyField) {
NSString *key = [_xorKeyField.text stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
self.tempConfig.xorKey = key.length > 0 ? key : nil;
// Cache for next time
if (key.length > 0) {
[[NSUserDefaults standardUserDefaults] setObject:key forKey:@"selly_xor_key_cache"];
} else {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"selly_xor_key_cache"];
}
}
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