之前有次需求是要求做cell内的输入框动态换行,顺便研究了一下UITextView的实现,核心是UIKeyInput
协议,重写
- (BOOL)hasText {
- (void)insertText:(NSString *)text {
- (void)deleteBackward {
这三个方法来完成输入框。第一个方法判断是否有文字,第二个是插入文本是的回调,第三个方法是删除按钮按下时的回调。
整个输入框通过SGEditViewDelegate.h
、SGEditView.h
、SGEditView.m
来实现。
SGEditViewDelegate.h
这个代理作用是模仿UITextView的部分协议方法,例如- (void)editViewDidChange:
等等,在合适的时机调用。
#import <Foundation/Foundation.h>
@class SGEditView;
/// Delegate for SGEditView, response to various event called.
@protocol SGEditViewDelegate <NSObject>
/// When EditView did change its text and executed this method.
- (void)editViewDidChange: (SGEditView *)editView;
@optional
/// When EditView
- (BOOL)editViewShouldReturn: (SGEditView *)editView inChange: (NSString *)text;
/// When EditView become first responsder and the Keyboard was presented into scene, executed this method.
- (void)editViewDidBeginEditing: (SGEditView *)editView;
/// When EditView processd all tasks after the Keyboard finished typing. executed this method.
- (void)editViewDidEndEditing: (SGEditView *)editView;
@end
SGEditView.h
对外暴露一些属性,例如占位文字,文字间隙等等
#import <UIKit/UIKit.h>
#import "SGEditViewDelegate.h"
NS_ASSUME_NONNULL_BEGIN
/// Self define EditView likes UITextView but based on UIView.
@interface SGEditView : UIView
/// Delegate target.
@property (nonatomic, weak) NSObject<SGEditViewDelegate> *delegate;
@property (nonatomic, strong) NSString *placeholder;
@property (nonatomic, strong) NSString *text;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) UIEdgeInsets textContainerInset;
@property (nonatomic, assign) BOOL isEnableEnterToLeave;
@end
NS_ASSUME_NONNULL_END
SGEditView.m
这里具体实现有些复杂,例如光标的位置计算就实现的有些问题,如果有读者平常业务是frame编写的就知道计算多行文字的末尾position是有多复杂,希望有经验的读者不吝赐教解决这个问题。
系统的UITextView输入框可以输入中文,但是中文存在候选文本,处理起来比较麻烦所以这里就没有实现。
具体实现原理就是:
touchBegin
里面注册第一响应者调起键盘;insertText
方法里面在合适的时机把代理方法回调出去;- 使用属性把文本保存起来,例如这里是用
_text
保存的; - 调用
[self setNeedsDisplay];
把drawRect
唤起; - 在
drawRect
里面使用CF相关内容绘制文字,最后别忘了由于是调用的c代码,不要忘了手动释放内存。
#import "SGEditView.h"
#import <CoreText/CoreText.h>
static NSString * const path_text = @"text";
static NSString * const path_cursor_opacity = @"opacity";
@interface SGEditView ()<UIKeyInput>
@property (nonatomic, strong) UILabel *placeholderLabel;
@property (nonatomic, strong) UIView *cursor;
@property (nonatomic, strong) CABasicAnimation *cursorAnimation;
@property (nonatomic, readonly) BOOL canBecomeFirstResponder;
@end
@implementation SGEditView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupAttributes];
}
return self;
}
- (void)dealloc {
[self removeObserver:self forKeyPath:path_text context:nil];
}
- (void)setupAttributes {
self.text = @"";
[self addSubview:self.placeholderLabel];
[self addObserver:self forKeyPath:path_text options:NSKeyValueObservingOptionNew context:nil];
[self addSubview:self.cursor];
self.cursor.frame = CGRectMake(0, 0, 2, self.font.lineHeight);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
if (self.canBecomeFirstResponder){
[self becomeFirstResponder];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:path_text]) {
self.placeholderLabel.hidden = self.text.length > 0;
}
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGRect insetRect = CGRectMake(rect.origin.x + self.textContainerInset.left,
rect.origin.y - self.textContainerInset.top,
rect.size.width - self.textContainerInset.left - self.textContainerInset.right,
rect.size.height - self.textContainerInset.bottom);
CGContextRef ctx = UIGraphicsGetCurrentContext();
NSDictionary *attrDictionary = @{
NSFontAttributeName: self.font,
NSForegroundColorAttributeName: self.textColor
};
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.text attributes:attrDictionary];
CFAttributedStringRef drawStr = CFBridgingRetain(attrString);
CTFramesetterRef setter = CTFramesetterCreateWithAttributedString(drawStr);
CGPathRef path = CGPathCreateWithRect(insetRect, NULL);
CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, CFAttributedStringGetLength(drawStr)), path, NULL);
CGContextSaveGState(ctx);
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, 0, -CGRectGetHeight(insetRect));
CTFrameDraw(frame, ctx);
CGContextRestoreGState(ctx);
CGPathRelease(path);
CFRelease(frame);
CFRelease(setter);
CFRelease(drawStr);
}
- (BOOL)hasText{
return self.text.length > 0;
}
- (void)insertText:(NSString *)text {
if ([self.delegate respondsToSelector:@selector(editViewDidBeginEditing:)]){
[self.delegate editViewDidBeginEditing:self];
}
if ([self.delegate respondsToSelector:@selector(editViewShouldReturn:inChange:)]){
if ([self.delegate editViewShouldReturn:self inChange:text] == YES){
return;
}
}
if ([self.text isEqualToString:@"\n"]){
[self resignFirstResponder];
[self setNeedsDisplay];
return;
}
self.text = [self.text stringByAppendingString:text];
CGPoint cursorPoint = [self getLiveTextLeadingLocationIn:self.text];
self.cursor.frame = CGRectMake(cursorPoint.x, cursorPoint.y, self.cursor.frame.size.width, self.cursor.frame.size.height);
if ([self.delegate respondsToSelector:@selector(editViewDidChange:)]){
[self.delegate editViewDidChange:self];
}
[self setNeedsDisplay];
if ([self.delegate respondsToSelector:@selector(editViewDidEndEditing:)]){
[self.delegate editViewDidEndEditing:self];
}
}
- (void)deleteBackward {
if (self.text.length > 0) {
self.text = [self.text substringToIndex:[self.text length] -1];
} else {
self.text = @"";
}
CGPoint cursorPoint = [self getLiveTextLeadingLocationIn:self.text];
self.cursor.frame = CGRectMake(cursorPoint.x, cursorPoint.y, self.cursor.frame.size.width, self.cursor.frame.size.height);
[self setNeedsDisplay];
}
- (void)setPlaceholder:(NSString *)placeholder {
self.placeholderLabel.text = placeholder;
}
- (UILabel *)placeholderLabel {
if (!_placeholderLabel){
_placeholderLabel = [[UILabel alloc] init];
_placeholderLabel.textColor = [UIColor lightGrayColor];
_placeholderLabel.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), 20);
}
return _placeholderLabel;
}
- (UIFont *)font {
if (!_font){
_font = [UIFont systemFontOfSize:16 weight:UIFontWeightLight];
}
return _font;
}
- (UIColor *)textColor {
if (!_textColor){
_textColor = [UIColor blackColor];
}
return _textColor;
}
- (UIView *)cursor {
if (!_cursor){
_cursor = [[UIView alloc] init];
_cursor.layer.cornerRadius = 1;
_cursor.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.4];
}
return _cursor;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (CGPoint)getLiveTextLeadingLocationIn: (NSString *)text {
const CGFloat MAX_SINGLE_W = CGRectGetWidth(self.frame) - self.textContainerInset.left - self.textContainerInset.right;
const CGFloat FONT_H = ceil(self.font.lineHeight);
CGFloat finalH = [self fixWidthGetHeight:text
andFont:self.font
andFixW:MAX_SINGLE_W] - FONT_H;
CGFloat realW = [self fixHeightGetWidth:text
andFont:self.font
andFixH:FONT_H];
CGFloat reminderW = reminder(realW, MAX_SINGLE_W);
CGFloat finalW = realW > MAX_SINGLE_W ? (reminderW + 3.5) : realW;
return CGPointMake(finalW, finalH);
}
- (CGFloat)fixWidthGetHeight: (NSString *) string andFont: (UIFont *)font andFixW: (CGFloat)w{
return [string boundingRectWithSize:CGSizeMake(w, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{
NSFontAttributeName: font }
context:nil].size.height;
}
- (CGFloat)fixHeightGetWidth: (NSString *) string andFont: (UIFont *)font andFixH: (CGFloat)h{
return [string boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, h)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{
NSFontAttributeName: font }
context:nil].size.width;
}
int reminder(CGFloat a, CGFloat b){
int c = a;
int d = b;
return c - (c / d) * d;
}
@end
ViewController中调用:
import UIKit
class ViewController: UIViewController, SGEditViewDelegate{
lazy var editView: SGEditView = {
let edit = SGEditView(frame: CGRect(x: 20, y: 200, width: 375 - 40, height: 100))
edit.backgroundColor = .gray
edit.placeholder = "Input accout."
edit.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 5, right: 10)
return edit
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(editView)
editView.delegate = self
}
func editViewDidBeginEditing(_ editView: SGEditView!) {
print("------> editViewDidBeginEditing: \(editView.text)")
}
func editViewShouldReturn(_ editView: SGEditView!, inChange text: String!) -> Bool {
if editView.text.contains("abc") {
return true
}
return false
}
func editViewDidChange(_ editView: SGEditView!) {
print("------> editViewDidChange: \(editView.text)")
}
func editViewDidEndEditing(_ editView: SGEditView!) {
print("------> editViewDidEndEditing: \(editView.text)")
}
}