当UITextFiled和UITextView这种文本输入类控件成为第一响应者时,弹出的键盘由他们的一个UIView类的inputView属性来控制,当inputView为nil时会弹出系统的键盘,想要弹出自定义的键盘,将我们自定义的UIView对象给inputView属性赋值即可。表情键盘重点在于排列各个表情和删除键,以及表情键盘上的各种回调设置;
下面为键盘预览图,兼容了竖屏各版本适配,横屏没有兼顾。横屏适配参见这篇博客iOS之自定义表情键盘
图1为6的常用表情,图2为6的全部表情,图3为5的全部表情,表情个数统一为7列3排,根据屏幕不同修改间距以及键盘高度;
下面为项目结构图:采用MVC模式,View层提供表情键盘以及自定义的UITextView。Model层提供表情数据。我为了简单就直接把聊天工具栏通过storyboard拖到了VC上,这里应该再封装一个toolView的;
1、首先来弄好数据层FaceManager,具有一个单例方法、声明了三个数组属性来存放不同的表情;表情图片由Face文件夹来提供;
AllFaces通过我一个名为“emoticons”的plist文件来获取,里面存放的是一个个表情字典,对应着Face中的图片名和图片文字描述;
RecentlyFaces是最近使用过的图片,从本地获取;BigFaces是用来扩展其他大型以及动态效果表情的,没有实现
@interface FacesManager : NSObject @property (nonatomic, strong, readonly)NSArray * RecentlyFaces;
@property (nonatomic, strong, readonly)NSArray * AllFaces;
@property (nonatomic, strong, readonly)NSArray * BigFaces; + (instancetype)share;
- (void)fetchRecentlyFaces;
@end #import "FacesManager.h" @implementation FacesManager +(instancetype)share
{
static FacesManager * m = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
m = [[FacesManager alloc] init];
});
return m ;
} - (instancetype)init
{
self = [super init];
if (self) {
[self fetchAllFaces];
[self fetchBigFaces];
}
return self;
} - (void)fetchAllFaces
{
NSString * path = [[NSBundle mainBundle] pathForResource:@"emoticons" ofType:@"plist"];
NSArray * arrFace = [NSArray arrayWithContentsOfFile:path];
_AllFaces = arrFace;
} - (void)fetchRecentlyFaces
{
NSUserDefaults * defauls = [NSUserDefaults standardUserDefaults];
NSArray * arrFace = [defauls objectForKey:@"RecentlyFaces"];
_RecentlyFaces = arrFace;
} - (void)fetchBigFaces
{ } @end
2、数据层弄好后,实现关键的FaceKeyBoardView;首先在这个view上我们需要向外发送:点击每个表情、发送键、删除键的事件,所以需要提供三个向外的回调接口:点击表情的回调、点击删除的回调、点击发送的回调;然后再分析视图结构,首先view上部贴了一个ScrollView用来滑动显示每一页表情、一个稍微靠下的的pageController用于显示当前页数、以及底部的toolBar;
FaceKeyBoardView.h
define了几个需要用到的参数。设置了三个回调接口;
#import <UIKit/UIKit.h>
#define GrayColor [UIColor colorWithRed:231 / 255.0 green:231 / 255.0 blue:231 / 255.0 alpha:1]
#define ScreenWidth [UIScreen mainScreen].bounds.size.width
#define ScreenHeight [UIScreen mainScreen].bounds.size.height
#define ToolBarHeight 40 typedef void (^FaceKeyBoardBlock)(NSString * faceName,NSInteger faceTag);
typedef void (^FaceKeyBoardSendBlock)(void);
typedef void (^FaceKeyBoardDeleteBlock)(void); @interface FaceKeyBoardView : UIView - (void)setFaceKeyBoardBlock:(FaceKeyBoardBlock)block;
- (void)setFaceKeyBoardSendBlock:(FaceKeyBoardSendBlock)block;
- (void)setFaceKeyBoardDeleteBlock:(FaceKeyBoardDeleteBlock)block;
@end
FaceKeyBoardView.m:
用到的属性:
@interface FaceKeyBoardView ()<UIScrollViewDelegate> {
CGFloat _FKBViewH;
} @property (nonatomic, strong)NSArray * arrFace;
@property (nonatomic, strong)UIScrollView * scFace;
@property (nonatomic, strong)FaceKeyBoardBlock block;
@property (nonatomic, strong)FaceKeyBoardSendBlock sendBlock;
@property (nonatomic, strong)FaceKeyBoardDeleteBlock deleteBlock;
@property (nonatomic, strong)UIToolbar * toolBar;
@property (nonatomic, strong)UIPageControl * pageC; @property (nonatomic, strong)FacesManager * FManager; @end
a、首先来重写view的init方法,在这里面设定好view的frame以及设定view的子控件。由于屏幕尺寸不同,所以表情竖直方向间距不同,就会影响到表情键盘的高度,所以frame是动态计算出来的;然后再loadview方法中初始化facemanager用来提供数据,以及获得全部表情和设置toolBar;
- (instancetype)init
{
self = [super init];
if (self) {
[self setViewFrame];
[self loadKeyBoardView];
}
return self;
} - (void)setViewFrame
{
CGFloat marginY = (ScreenWidth - * ) / ( + );
CGFloat scViewH = * ( + marginY) + marginY* + ;
_FKBViewH = scViewH + ToolBarHeight;
self.frame = CGRectMake(, ScreenHeight - _FKBViewH, ScreenWidth, _FKBViewH);
} - (void)loadKeyBoardView
{
//初始化manager
self.FManager = [FacesManager share];
//获取数据
[self fetchAllFaces];
//设置toolBar
[self setToolBar];
}
b、设置了三个方法来获取不同的表情数据用于显示不同的表情键盘;第一次时默认执行fetchAllFaces;
- (void)fetchRecentlyFaces
{
//更新manager
[self.FManager fetchRecentlyFaces];
self.arrFace = self.FManager.RecentlyFaces;
[self setFaceFrame];
} - (void)fetchAllFaces
{
self.arrFace = self.FManager.AllFaces;
//设置表情scrollView
[self setFaceFrame];
} - (void)fetchBigFaces
{
self.arrFace = nil;
[self setFaceFrame];
}
c、在setFaceFrame方法中设置scrollView以及pageController;我通过表情数量来循环设置每个表情按钮的位置,固定了7列三行,根据屏幕设置不同的间距,在每一页的右下角设置删除按钮。根据页数来设置scrollView的内容宽度以及pageController的页数;
- (void)setFaceFrame
{
//列数
NSInteger colFaces = ;
//行数
NSInteger rowFaces = ;
//设置face按钮frame
CGFloat FaceW = ;
CGFloat FaceH = ;
CGFloat marginX = (ScreenWidth - colFaces * FaceW) / (colFaces + );
CGFloat marginY = marginX;
NSLog(@"%lf",marginX); //表情数量
NSInteger FaceCount = self.arrFace.count;
//每页表情数和scrollView页数;
NSInteger PageFaceCount = colFaces * rowFaces ;
NSInteger SCPages = FaceCount / PageFaceCount + ; CGFloat scViewH = rowFaces * (FaceH + marginY) + marginY* + ;
//初始化scrollView
self.scFace = [[UIScrollView alloc] initWithFrame:CGRectMake(, , ScreenWidth, scViewH)];
self.scFace.contentSize = CGSizeMake(ScreenWidth * SCPages, scViewH);
self.scFace.pagingEnabled = YES;
self.scFace.bounces = NO;
self.scFace.delegate = self;
[self addSubview:self.scFace];
//初始化贴在sc上的view
UIView * BtnView = [[UIView alloc] init];
BtnView.frame = CGRectMake(, , ScreenWidth * SCPages, scViewH);
[BtnView setBackgroundColor:GrayColor];
[self.scFace addSubview:BtnView]; for (NSInteger i = ; i < FaceCount + SCPages; i ++)
{
//当前页数
NSInteger currentPage = i / PageFaceCount;
//当前行
NSInteger rowIndex = i / colFaces - (currentPage * rowFaces);
//当前列
NSInteger colIndex = i % colFaces; //viewW * currentPage换页
CGFloat btnX = marginX + colIndex * (FaceW + marginX) + ScreenWidth * currentPage;
CGFloat btnY = rowIndex * (marginY + FaceH) + marginY;
if ((i - (currentPage + ) * (PageFaceCount - ) == currentPage || i == FaceCount + SCPages - ) && self.arrFace)
{
//创建删除按钮
CGFloat btnDelteX = (currentPage + ) * ScreenWidth - (marginX + FaceW);
CGFloat btnDelteY = * (FaceH + marginY) +marginY; UIButton * btnDelte = [UIButton buttonWithType:UIButtonTypeSystem];
btnDelte.frame = CGRectMake(btnDelteX, btnDelteY, FaceW, FaceH);
[btnDelte setBackgroundImage:[UIImage imageNamed:@"icon_delete-2"] forState:UIControlStateNormal];
[btnDelte setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
btnDelte.titleLabel.font = [UIFont boldSystemFontOfSize:]; [btnDelte addTarget:self action:@selector(tapDeleteBtn) forControlEvents:UIControlEventTouchUpInside]; [BtnView addSubview:btnDelte];
}
else
{
//创建face按钮
UIButton * btn = [[UIButton alloc] init];
btn.frame = CGRectMake(btnX , btnY, FaceW, FaceH);
//tga
btn.tag = i - currentPage;
//按钮回调;
[btn addTarget:self action:@selector(tapFaceBtnWithButton:) forControlEvents:UIControlEventTouchUpInside];
NSString * strIMG = self.arrFace[i - currentPage][@"png"];
[btn setImage:[UIImage imageNamed:strIMG] forState:UIControlStateNormal];
[BtnView addSubview:btn];
}
} //创建pageController
CGFloat pageH = ;
CGFloat pageW = ScreenWidth;
CGFloat pageX = ;
CGFloat pageY = scViewH - pageH - marginY;
self.pageC = [[UIPageControl alloc] initWithFrame:CGRectMake(pageX, pageY, pageW, pageH)];
self.pageC.numberOfPages = SCPages;
self.pageC.currentPage = ;
self.pageC.pageIndicatorTintColor = [UIColor lightGrayColor];
self.pageC.currentPageIndicatorTintColor = [UIColor grayColor];
[self addSubview:self.pageC];
}
b、还需要把c中的各种点击事件和scrollView的代理事件实现
点击表情的事件中我执行了2个操作:1、将点击的表情存到本地常用表情数组中,逻辑为,如果数组中已有这个表情,就将此表情移到最前面,没有就将表情插入到数组第一位。这里我想着数据量不是很大就使用了Preference来存放本地;2、将表情的文字描述和表情按钮的tga通过block传出去;
当scrollView翻动时,让pageController的当前页数跟着变化;点击删除发送回调;
//点击表情
- (void)tapFaceBtnWithButton:(UIButton *)button
{
//将表情存储为常用表情
NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray * arrFaces = (NSMutableArray *)[defaults objectForKey:@"RecentlyFaces"]; if (!arrFaces)
{
arrFaces = [NSMutableArray array];
NSDictionary * dicFace = @{@"png":self.arrFace[button.tag][@"png"],@"faceTag":@(button.tag),@"chs":self.arrFace[button.tag][@"chs"]};
[arrFaces addObject:dicFace];
[defaults setObject:arrFaces forKey:@"RecentlyFaces"];
[defaults synchronize];
}
//NSLog(@"%p",arrFaces);
else
{
//需要新建一个可变数组,不然修改数组会报错。
NSMutableArray * arrM = [NSMutableArray arrayWithArray:arrFaces];
BOOL isHaveSameFace = NO;
for (NSDictionary * dic in arrFaces)
{
//NSLog(@"%ld--%ld",button.tag,[dic[@"faceTag"] integerValue]);
NSString * strFace = self.arrFace[button.tag][@"chs"];
NSString * strFaceDic = dic[@"chs"];
if ([strFace isEqualToString:strFaceDic])
{
[arrM removeObject:dic];
NSLog(@"%@",dic);
//后添加的排在前面;
[arrM insertObject:dic atIndex:];
isHaveSameFace = YES;
}
}
if (!isHaveSameFace)
{
NSDictionary * dicFace = @{@"png":self.arrFace[button.tag][@"png"],@"faceTag":@(button.tag),@"chs":self.arrFace[button.tag][@"chs"]};
[arrM insertObject:dicFace atIndex:];
}
[defaults setObject:arrM forKey:@"RecentlyFaces"];
[defaults synchronize];
}
//block传值
self.block(self.arrFace[button.tag][@"chs"],button.tag);
} //点击删除
- (void)tapDeleteBtn
{
self.deleteBlock();
} -(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
self.pageC.currentPage = targetContentOffset->x / ScreenWidth;
}
d、设置toolBar、然后将各个按钮的点击事件实现:
- (void)setToolBar
{
self.toolBar = [[UIToolbar alloc] initWithFrame:CGRectMake(, self.scFace.frame.size.height, ScreenWidth, ToolBarHeight)];
self.toolBar.backgroundColor = GrayColor; [self addSubview:self.toolBar];
UIBarButtonItem * spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem * recentlyFaceItem = [[UIBarButtonItem alloc] initWithTitle:@"最近表情" style:UIBarButtonItemStylePlain target:self action:@selector(tapRecentlyFaceBtn)];
UIBarButtonItem * normalFaceItem = [[UIBarButtonItem alloc] initWithTitle:@"普通" style:UIBarButtonItemStylePlain target:self action:@selector(tapNormalFaceBtn)];
UIBarButtonItem * bigFaceItem = [[UIBarButtonItem alloc] initWithTitle:@"大表情" style:UIBarButtonItemStylePlain target:self action:@selector(tapBigFaceBtn)];
UIBarButtonItem * sendItem = [[UIBarButtonItem alloc] initWithTitle:@"发送" style:UIBarButtonItemStylePlain target:self action:@selector(tapSendBtn)]; [self.toolBar setItems:[NSArray arrayWithObjects:recentlyFaceItem,spaceItem,normalFaceItem,spaceItem,bigFaceItem,spaceItem,sendItem, nil]];
} //点击ToolBar上的按钮回调
- (void)tapRecentlyFaceBtn
{
[self fetchRecentlyFaces];
}
- (void)tapSendBtn
{
self.sendBlock();
}
- (void)tapBigFaceBtn{
[self fetchBigFaces];
}
- (void)tapNormalFaceBtn
{
[self fetchAllFaces];
}
e、还要实现三个设置回调接口的方法、此时faceKeyBoardView中的方法都实现了;
//点击表情接口
- (void)setFaceKeyBoardBlock:(FaceKeyBoardBlock)block
{
self.block = block;
}
//发送接口
-(void)setFaceKeyBoardSendBlock:(FaceKeyBoardSendBlock)block
{
self.sendBlock = block;
}
//删除接口
-(void)setFaceKeyBoardDeleteBlock:(FaceKeyBoardDeleteBlock)block
{
self.deleteBlock = block;
}
3、自定义一个UITextView、也可以使用UITextFiledView
.h:具有一个发送回调以及切换键盘状态的方法:
#import "XQGTextView.h"
#import "FaceKeyBoardView.h" @interface XQGTextView () @property (nonatomic, strong)FaceKeyBoardView * viewFaceKB;
@property (nonatomic, strong)SendBlock block;
@end
.m:实现方法以及初始化表情键盘,在faceKeyBoard发送回调和删除回调中实现需要做的事;
@implementation XQGTextView - (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self loadFaceKeyBoardView];
}
return self;
} - (void)awakeFromNib
{
[self loadFaceKeyBoardView];
} - (void)loadFaceKeyBoardView
{
self.viewFaceKB = [[FaceKeyBoardView alloc] init]; __weak __block XQGTextView * copy_self = self; [self.viewFaceKB setFaceKeyBoardBlock:^(NSString *faceName, NSInteger faceTag) {
copy_self.text = [copy_self.text stringByAppendingString:faceName];
}]; [self.viewFaceKB setFaceKeyBoardSendBlock:^{
copy_self.block();
//清空textview
copy_self.text = nil;
}];
[self.viewFaceKB setFaceKeyBoardDeleteBlock:^{
NSMutableString * string = [[NSMutableString alloc] initWithString:copy_self.text];
[string deleteCharactersInRange:NSMakeRange(copy_self.text.length - , )];
copy_self.text = string;
}];
} -(void)changeKeyBoard
{
if (self.inputView != nil)
{
self.inputView = nil;
[self reloadInputViews];
}
else
{
self.inputView = self.viewFaceKB;
[self reloadInputViews];
}
} - (void)setFaceKeyBoard
{
self.inputView = self.viewFaceKB;
} - (void)setSendBlock:(SendBlock)block
{
self.block = block;
} @end
4、在VC中进行操作:
a、在storyBoard中构建聊天工具栏:将textView绑定为我自定义的textView;
b、在发送回调中进行发送消息,利用正则表达式解析出表情发送图文混排的消息;以及聊天工具栏跟随键盘高度变化而变化的设置等;点击表情按钮弹出表情键盘,键盘切换;代码中都有详细描述;
.m:
#import "MainViewController.h"
#import "XQGTextView.h" @interface MainViewController ()<UITextViewDelegate> @property (weak, nonatomic) IBOutlet XQGTextView *viewText;
@property (weak, nonatomic) IBOutlet UIView *viewChatToolBar;
@property (weak, nonatomic) IBOutlet UILabel *lalText; @end @implementation MainViewController - (void)viewDidLoad {
[super viewDidLoad];
//清空常用表情
// NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
// NSMutableArray * arrFaces = [defaults objectForKey:@"RecentlyFaces"];
// arrFaces = nil;
// [defaults setObject:arrFaces forKey:@"RecentlyFaces"]; self.viewText.delegate = self;
//发送回调
[self.viewText setSendBlock:^{
[self sendPictureAndText];
}];
//监听键盘弹出的通知
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(KeyBoardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
} //发送图文
- (void)sendPictureAndText
{
//正则表达式取出表情
NSString * str = self.viewText.text;
NSMutableAttributedString * strAtt = [[NSMutableAttributedString alloc] initWithString:str];
//创建匹配正则表达式类型描述模板
NSString * pattern = @"\\[[a-zA-Z0-9\\u4e00-\\u9fa5]+\\]";
//依据正则表达式创建匹配对象
NSError * error = nil;
//CaseInsensitive
NSRegularExpression * regular = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
if (regular == nil)
{
NSLog(@"正则创建失败");
NSLog(@"%@",error.localizedDescription);
return;
}
//把搜索出来的结果存到数组中
NSArray * result = [regular matchesInString:strAtt.string options:NSMatchingReportCompletion range:NSMakeRange(, strAtt.string.length)]; NSString * path = [[NSBundle mainBundle] pathForResource:@"emoticons.plist" ofType:nil];
NSArray * arrPlist = [NSArray arrayWithContentsOfFile:path]; for (NSInteger i = result.count - ; i >= ; i--)
{
NSTextCheckingResult * r = result[i];
//NSLog(@"%@",NSStringFromRange(r.range));
NSString * imageStr = [strAtt.string substringWithRange:r.range];
//NSLog(@"%@",imageStr); for (NSDictionary * dic in arrPlist)
{
if ([dic[@"chs"] isEqualToString:imageStr])
{
NSTextAttachment * textAtt = [[NSTextAttachment alloc] init];
textAtt.image = [UIImage imageNamed:dic[@"png"]];
NSAttributedString * strImage = [NSAttributedString attributedStringWithAttachment:textAtt];
[strAtt replaceCharactersInRange:r.range withAttributedString:strImage];
}
}
}
self.lalText.attributedText = strAtt;
} //监听键盘弹出的方法
-(void)KeyBoardWillChangeFrame: (NSNotification *)noteInfo
{
//获取键盘的Y值
CGRect keySize = [noteInfo.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGFloat keyY = keySize.origin.y;
//让view跟随键盘移动
CGFloat viewY = keyY - self.view.bounds.size.height;
//让view变化和键盘变化一致
self.view.transform = CGAffineTransformMakeTranslation(, viewY);
} -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch * touch = [touches anyObject];
if ([touch.view isEqual:self.view]) {
[self.view endEditing:YES];
}
}
//监控编辑结束状态
-(void)textViewDidEndEditing:(UITextView *)textView
{
self.viewText.inputView = nil;
} - (IBAction)tapVoice:(UIButton *)sender {
NSLog(@"切换语音");
} - (IBAction)tapFace:(UIButton *)sender
{
//如果还没弹出键盘就直接弹出表情键盘;弹出了就改变键盘样式
if (self.viewText.isFirstResponder)
{
[self.viewText changeKeyBoard];
}
else
{
[self.viewText setFaceKeyBoard];
[self.viewText becomeFirstResponder];
}
} - (IBAction)tapMoreFunction:(UIButton *)sender {
NSLog(@"更多功能");
} @end
这样,自定义表情键盘就实现了,聊天工具栏还需要进行进一步的封装;