如何为JVFloatLabeledTextField实现类似SkyFloatingLabelTextField的动态下划线?
实现JVFloatLabeledTextField的动态下划线效果
嘿,我之前也折腾过用JVFloatLabeledTextField实现类似SkyFloatingLabelTextField的动态下划线,给你分享两个实用的方案,直接基于现有库扩展就行,不用换库~
方案一:基础动态下划线(颜色+高度过渡)
这个方案实现编辑时下划线变粗变色、结束编辑时根据文本状态恢复的效果,代码简单易上手:
步骤1:自定义JVFloatLabeledTextField子类
创建一个子类,封装下划线的逻辑,避免污染ViewController:
#import "JVFloatLabeledTextField.h" @interface CustomFloatTextField : JVFloatLabeledTextField @property (nonatomic, strong) CALayer *bottomBorderLayer; @end @implementation CustomFloatTextField - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setupBottomBorder]; } return self; } // 初始化下划线和事件监听 - (void)setupBottomBorder { // 移除默认边框,避免和自定义下划线冲突 self.borderStyle = UITextBorderStyleNone; // 创建基础下划线layer self.bottomBorderLayer = [CALayer layer]; CGFloat initialHeight = 1.0; self.bottomBorderLayer.frame = CGRectMake(0, self.frame.size.height - initialHeight, self.frame.size.width, initialHeight); self.bottomBorderLayer.backgroundColor = [UIColor lightGrayColor].CGColor; [self.layer addSublayer:self.bottomBorderLayer]; // 监听编辑状态变化 [self addTarget:self action:@selector(onEditingBegin:) forControlEvents:UIControlEventEditingDidBegin]; [self addTarget:self action:@selector(onEditingEnd:) forControlEvents:UIControlEventEditingDidEnd]; } // 开始编辑时的动画:变粗+变色 - (void)onEditingBegin:(UITextField *)textField { // 颜色动画 CABasicAnimation *colorAnim = [CABasicAnimation animationWithKeyPath:@"backgroundColor"]; colorAnim.fromValue = (__bridge id)[UIColor lightGrayColor].CGColor; colorAnim.toValue = (__bridge id)[UIColor systemBlueColor].CGColor; colorAnim.duration = 0.3; // 高度动画 CABasicAnimation *heightAnim = [CABasicAnimation animationWithKeyPath:@"bounds.size.height"]; heightAnim.fromValue = @1.0; heightAnim.toValue = @2.0; heightAnim.duration = 0.3; [self.bottomBorderLayer addAnimation:colorAnim forKey:@"colorTransition"]; [self.bottomBorderLayer addAnimation:heightAnim forKey:@"heightTransition"]; // 更新layer最终状态(动画只是视觉效果,必须手动更新属性) self.bottomBorderLayer.backgroundColor = [UIColor systemBlueColor].CGColor; self.bottomBorderLayer.frame = CGRectMake(0, self.frame.size.height - 2.0, self.frame.size.width, 2.0); } // 结束编辑时的状态恢复 - (void)onEditingEnd:(UITextField *)textField { if (textField.text.length == 0) { // 文本为空,恢复初始样式 CABasicAnimation *colorAnim = [CABasicAnimation animationWithKeyPath:@"backgroundColor"]; colorAnim.fromValue = (__bridge id)[UIColor systemBlueColor].CGColor; colorAnim.toValue = (__bridge id)[UIColor lightGrayColor].CGColor; colorAnim.duration = 0.3; CABasicAnimation *heightAnim = [CABasicAnimation animationWithKeyPath:@"bounds.size.height"]; heightAnim.fromValue = @2.0; heightAnim.toValue = @1.0; heightAnim.duration = 0.3; [self.bottomBorderLayer addAnimation:colorAnim forKey:@"colorReverse"]; [self.bottomBorderLayer addAnimation:heightAnim forKey:@"heightReverse"]; self.bottomBorderLayer.backgroundColor = [UIColor lightGrayColor].CGColor; self.bottomBorderLayer.frame = CGRectMake(0, self.frame.size.height - 1.0, self.frame.size.width, 1.0); } else { // 保留高亮颜色,仅恢复高度 CABasicAnimation *heightAnim = [CABasicAnimation animationWithKeyPath:@"bounds.size.height"]; heightAnim.fromValue = @2.0; heightAnim.toValue = @1.0; heightAnim.duration = 0.3; [self.bottomBorderLayer addAnimation:heightAnim forKey:@"heightReverse"]; self.bottomBorderLayer.frame = CGRectMake(0, self.frame.size.height - 1.0, self.frame.size.width, 1.0); } } // 适配屏幕旋转或frame变化 - (void)layoutSubviews { [super layoutSubviews]; CGFloat borderHeight = (self.isEditing || self.text.length > 0) ? 2.0 : 1.0; self.bottomBorderLayer.frame = CGRectMake(0, self.frame.size.height - borderHeight, self.frame.size.width, borderHeight); } @end
步骤2:替换原有JVFloatLabeledTextField
在你的项目中,把原本的JVFloatLabeledTextField实例替换成CustomFloatTextField即可,不管是代码创建还是xib/storyboard里修改类名。
方案二:滑动填充式下划线(更贴近Sky的效果)
如果想要实现SkyFloatingLabelTextField里那种下划线从左到右填充颜色的动画,可以用Layer Mask来实现:
#import "JVFloatLabeledTextField.h" @interface CustomFloatTextField : JVFloatLabeledTextField @property (nonatomic, strong) CALayer *defaultBorderLayer; // 初始灰色下划线 @property (nonatomic, strong) CALayer *activeBorderLayer; // 高亮蓝色下划线 @property (nonatomic, strong) CALayer *borderMaskLayer; // 用于动画的遮罩层 @end @implementation CustomFloatTextField - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setupAnimatedBorder]; } return self; } - (void)setupAnimatedBorder { self.borderStyle = UITextBorderStyleNone; // 1. 初始灰色下划线 self.defaultBorderLayer = [CALayer layer]; self.defaultBorderLayer.frame = CGRectMake(0, self.frame.size.height - 1.0, self.frame.size.width, 1.0); self.defaultBorderLayer.backgroundColor = [UIColor lightGrayColor].CGColor; [self.layer addSublayer:self.defaultBorderLayer]; // 2. 高亮蓝色下划线(初始被遮罩隐藏) self.activeBorderLayer = [CALayer layer]; self.activeBorderLayer.frame = CGRectMake(0, self.frame.size.height - 2.0, self.frame.size.width, 2.0); self.activeBorderLayer.backgroundColor = [UIColor systemBlueColor].CGColor; [self.layer addSublayer:self.activeBorderLayer]; // 3. 遮罩层:初始宽度为0,只显示activeBorderLayer的左侧一小部分 self.borderMaskLayer = [CALayer layer]; self.borderMaskLayer.backgroundColor = [UIColor whiteColor].CGColor; self.borderMaskLayer.frame = CGRectMake(0, 0, 0, 2.0); self.activeBorderLayer.mask = self.borderMaskLayer; // 监听编辑事件 [self addTarget:self action:@selector(onEditingBegin:) forControlEvents:UIControlEventEditingDidBegin]; [self addTarget:self action:@selector(onEditingEnd:) forControlEvents:UIControlEventEditingDidEnd]; } - (void)onEditingBegin:(UITextField *)textField { self.defaultBorderLayer.hidden = YES; // 动画遮罩层宽度从0到全屏,实现从左到右填充的效果 CABasicAnimation *widthAnim = [CABasicAnimation animationWithKeyPath:@"bounds.size.width"]; widthAnim.fromValue = @0; widthAnim.toValue = @(self.frame.size.width); widthAnim.duration = 0.4; [self.borderMaskLayer addAnimation:widthAnim forKey:@"maskExpand"]; self.borderMaskLayer.frame = CGRectMake(0, 0, self.frame.size.width, 2.0); } - (void)onEditingEnd:(UITextField *)textField { if (textField.text.length == 0) { // 文本为空,动画遮罩层收缩回0,然后显示初始下划线 CABasicAnimation *widthAnim = [CABasicAnimation animationWithKeyPath:@"bounds.size.width"]; widthAnim.fromValue = @(self.frame.size.width); widthAnim.toValue = @0; widthAnim.duration = 0.4; widthAnim.delegate = self; [self.borderMaskLayer addAnimation:widthAnim forKey:@"maskShrink"]; self.borderMaskLayer.frame = CGRectMake(0, 0, 0, 2.0); } else { // 保留高亮下划线,遮罩层保持全屏 self.borderMaskLayer.frame = CGRectMake(0, 0, self.frame.size.width, 2.0); } } // 动画结束后恢复初始下划线显示 - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { if (flag && [anim isEqual:[self.borderMaskLayer animationForKey:@"maskShrink"]]) { self.defaultBorderLayer.hidden = NO; } } - (void)layoutSubviews { [super layoutSubviews]; // 更新各layer的frame以适配布局变化 self.defaultBorderLayer.frame = CGRectMake(0, self.frame.size.height - 1.0, self.frame.size.width, 1.0); self.activeBorderLayer.frame = CGRectMake(0, self.frame.size.height - 2.0, self.frame.size.width, 2.0); self.borderMaskLayer.frame = CGRectMake(0, 0, (self.isEditing || self.text.length > 0) ? self.frame.size.width : 0, 2.0); } @end
注意事项
- 记得将所有
JVFloatLabeledTextField的borderStyle设为UITextBorderStyleNone,避免默认边框和自定义下划线冲突; - 动画的颜色、时长、下划线高度都可以根据你的UI设计自由调整;
- 如果需要适配多语言或动态字体,记得在
layoutSubviews里同步更新layer的状态;
内容的提问来源于stack exchange,提问作者SwiftNinja95
相关产品推荐
相关产品推荐

