背景
公司的IM模块一直都是我在开发和维护,几经改版已经和初始的设计完全不同了,业务也跟以前不尽相同,以前B、C两端共用的聊天页面也都经过各自的产品改造过了,输入框的设计也都按照各自的风格重新设计,这个版本要改造B端的IM,看着很乱的代码打算重构IM的业务模块。基本的思路就是抽出一份B、C都能共有的公共父类,去做类似于:基础页面的展示,加载消息,收发消息之类的基础功能,而基础页面就包括了上面的tableView和下面的输入框。在做IM重构之前首先要重构输入框,而输入框这个看似简单的模块,其实承载的业务还是很多的。想要开发一套易维护,易拓展的输入框也是需要下点功夫去做的。
需求分析
先来看看市面上多数app的输入框长什么样
QQ 微信 淘宝
可以看的出输入框的样式上还是有很多的,而且表现形式也是千差万别,还有类似于淘宝这种在输入框上有承载了其他功能的,但是核心的功能输入基本是一样的。而其他的功能类似于语音、表情、菜单这些功能是通过不同的表现形式附加在输入文本框的周围的。
使用装饰者模式实现一个输入框
通过上面的分析我们知道一个输入框的基本组成部分了。就是通过一个核心的文本输入框加上其他多个功能模块组成。这里就要引出装饰者模式的概念了:
装饰者模式(Decorator Pattern):动态的个一个对象增加一些额外的职责,就增加对象功能来说,装饰者模式比生成子类实现更为灵活,装饰者模式是一种对象结构型模式。
先来看类图:
装饰者模式.jpg
角色分析
Component(抽象构件):它是具体构件和抽象装饰类的共同父类或者是Protocol,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
具体到我的类中的定义为LGInputToolBarProtocol
typedef void(^AnimationCompleteBlock)(void);
@protocol LGInputBarDecoratorProtocol;
@protocol LGInputToolBarProtocol <NSObject>
@optional
/** 整体的高度 */
@property (nonatomic, assign, readonly) float totalHeight;
/** 实体view */
@property (nonatomic, strong, readonly) UIView <LGInputToolBarProtocol>*entityView;
/** 输入框(文本输入控件的父视图) */
@property (nonatomic, strong, readonly) UIView *textInputContainerView;
/** 文本输入控件 */
@property (nonatomic, strong, readonly) LGTextInputView *textInputView;
//添加装饰器
- (void)addDecorator:(id<LGInputBarDecoratorProtocol>)decorator;
//重置到初始状态
- (void)resetInputContainerViewToOriginal;
//弹起键盘
- (void)becomeFirstResponderForDecorator:(id<LGInputBarDecoratorProtocol>)decorator complete:(AnimationCompleteBlock)completeBlock;
//收起键盘,表情,菜单
- (void)resignFirstResponderForDecorator:(id<LGInputBarDecoratorProtocol>)decorator complete:(AnimationCompleteBlock)completeBlock;
//弹起view
- (void)popView:(UIView *)popView withFold:(BOOL)fold complete:(AnimationCompleteBlock)completeBlock;
//更新config
- (void)updateConfig:(LGTextInputViewConfig *)config;
//设置草稿
- (void)setDraftWithText:(NSString *)draftString;
@end
ConcreteComponent(具体构件):它是抽象构件类的子类,或者遵循协议。用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
具体到我的类中定义为LGInputToolBar
这个类实现了一个基础的带有文本输入功能的输入框,处理键盘弹起、收回,文本输入,输入框高度变化,键盘高度变化等。
大体的实现思路是把页面分为三个部分,
- 上: textView,还有承载装饰类传递过来的类似于语音、表情、菜单按钮等各种icon的view;
- 中: 中间部分view负责监听键盘变化、还有装饰类触发的高度变化的view;
- 下: 下边部分就是负责适配iPhone x之后的全面屏的底部安全区的;
通过这三个部分的高度相加就是这个输入框的整体高度。而每次高度变化的时候也是取的这个整体高度使得高度变化处理都是统一的。
当然文本输入框也需要单独封装一个类LGTextInputView
这个类的主要职责就是监听键盘高度变化的通知,还有文本宽高度变化的处理,然后回调给LGInputToolBar
然后通过一个confi类来配置这个输入框的基本样式LGTextInputViewConfig
@interface LGTextInputViewConfig : NSObject
@property (nonatomic, assign) UIEdgeInsets contentInsets;
@property (nonatomic, assign) UIEdgeInsets textContentInsets;
@property (nonatomic, assign) float contentHeight;
@property (nonatomic, strong) UIColor *backgroundColor;
@property (nonatomic, strong) UIColor *textBackgroundColor;
@property (nonatomic, assign) float cornerRadius;
+ (instancetype)defaultConfig;
@end
这个类定义了一些决定输入框位置和样式的属性,还有一个适用于我们app的默认config。
这样一个基础的文本输入框就实现了,长这个样子:
输入框.png
这个就是通过默认config初始化的输入框。
Decorator(抽象装饰类):它也是抽象构件类的子类,或者遵循协议,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
这里我增加了一个修饰类的协议, 为了统一具体修饰类的行为。
@protocol LGInputBarDecoratorProtocol <NSObject>
//添加到被修饰视图上面的view
@property (nonatomic, strong, readonly) UIView *appendView;
//选中状态
@property (nonatomic, assign) BOOL selected;
//被修饰视图通知修饰器视图更新frame
- (void)inputBarDidLayoutSubviews;
//重置到初始状态
- (void)resetToOriginalStatus;
@end
@interface LGInputToolBarAbstractDecorator : NSObject<LGInputToolBarProtocol, LGInputBarDecoratorProtocol>
@property (nonatomic, weak) id<LGInputToolBarProtocol> inputToolBar;
+ (instancetype)decoratorFor:(id<LGInputToolBarProtocol>)inputToolBar;
@end
@implementation LGInputToolBarAbstractDecorator
@synthesize selected = _selected;
#pragma mark - Lifecycle 生命周期
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
+ (instancetype)decoratorFor:(id<LGInputToolBarProtocol>)inputToolBar {
LGInputToolBarAbstractDecorator *decorator = [self new];
decorator.inputToolBar = inputToolBar;
return decorator;
}
#pragma Protocol - LGInputToolBarProtocol
- (UIView<LGInputToolBarProtocol> *)entityView {
return self.inputToolBar.entityView;
}
- (float)totalHeight {
return self.inputToolBar.totalHeight;
}
- (UIView *)textInputContainerView {
return self.inputToolBar.textInputContainerView;
}
- (LGIMTextInputView *)textInputView {
return self.inputToolBar.textInputView;
}
- (void)addDecorator:(id<LGInputBarDecoratorProtocol>)decorator {
[self.inputToolBar addDecorator:decorator];
}
- (void)resetInputContainerViewToOriginal {
[self.inputToolBar resetInputContainerViewToOriginal];
}
- (void)becomeFirstResponderForDecorator:(id<LGInputBarDecoratorProtocol>)decorator complete:(AnimationCompleteBlock)completeBlock {
[self.inputToolBar becomeFirstResponderForDecorator:decorator complete:completeBlock];
}
- (void)resignFirstResponderForDecorator:(id<LGInputBarDecoratorProtocol>)decorator complete:(AnimationCompleteBlock)completeBlock {
[self.inputToolBar resignFirstResponderForDecorator:decorator complete:completeBlock];
}
- (void)popView:(UIView *)popView withFold:(BOOL)fold complete:(AnimationCompleteBlock)completeBlock {
[self.inputToolBar popView:popView withFold:fold complete:completeBlock];
}
//更新config
- (void)updateConfig:(LGTextInputViewConfig *)config {
[self.inputToolBar updateConfig:config];
}
//设置草稿
- (void)setDraftWithText:(NSString *)draftString {
[self.inputToolBar setDraftWithText:draftString];
}
#pragma Protocol - LGInputToolBarProtocol
- (UIView *)appendView {
return nil;
}
- (void)setSelected:(BOOL)selected {
_selected = selected;
}
- (BOOL)selected {
return _selected;
}
- (void)inputBarDidLayoutSubviews {}
- (void)resetToOriginalStatus {}
这里定义了一些默认实现。可以理解为子类调用的一些行为最后都会调用到被装饰的类LGInputToolBar
ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
对于我们的需求来说需要增加四种装饰类:语音、表情、菜单、快捷回复。
每一个装饰类初始化的时候创建一个icon通过addDecorator:方法把icon放到被装饰的类上,然后被装饰的类在layoutSubviews方法中通知所有加入到自己类中的装饰类页面更新了。装饰类在收到通知后自己更新自己icon的位置。
每一个装饰类的行为都自己控制,比如点击了语音按钮,就在文本输入框上盖一个语音输入条,点击了表情按钮就创建一个表情键盘然后调用被装饰类提供的方法把表情键盘对象传递过去展示,点击了菜单按钮弹出菜单等,被装饰类只是提供了一套统一的行为模式供装饰类去调用。
这样通过多个装饰器类的组合就构成一个完整的输入框视图。
输入框完整版.png
这样我们就可以在IM中的公共父类中只放一个基本的不带装饰类的基础功能输入框了,之后B、C端各自根据自己的需求放置不同的装饰类去创建自己需要的输入框了,以后如果有功能修改的话只需要替换或者修改修饰类就可以了。
最后附上类图
装饰者模式InputBar.png
网友评论