效果展示

實現(xiàn)的功能
- 支持將表情轉(zhuǎn)換成字符串, 同時也可以將帶有表情的字符串轉(zhuǎn)換成表情圖片
- 可自定義表情包, 可自定義每頁表情的行數(shù)和列數(shù), 自定義表情包需要兩步
1: 添加表情包到EmojiPackage.bundle目錄下
2: 按照demo中的格式修改EmojiPackageList.plist文件- 支持長按預(yù)覽, 大表情支持gif, 刪除表情
- YBEmojiTextView實現(xiàn)了拷貝粘貼剪切功能, 所以如果需要支持該功能, 輸入框需要繼承自該類
- 支持修改部分外觀, 具體請查看YBEmojiConfig.h文件
- 適配iPhone X
思路
數(shù)據(jù)的處理
<1> 表情數(shù)據(jù)使用plist文件存儲, 根目錄是一個數(shù)組, 每一個對象是一個表情包字典, 每一個表情包中有表情包的屬性, 包括
- cover_pic: 表情包封面圖片, 要放在對應(yīng)的表情目錄下
- folderName: 表情圖片對應(yīng)的文件夾名字, 封面圖片就放在該目錄下
- isLargeEmoji: 是否是大表情
- title: 標(biāo)題, 暫時沒用到, 可用于以后擴(kuò)展
- emojis: 表情數(shù)組, 每一個對象是一個字典, 里邊有兩個字段
desc: 表情對應(yīng)的字符串, 用來在長按或者大表情下方顯示, 文字提取自[/和]中間的字符串, 例如: ??對應(yīng)的字符串就是[/哈哈]格式一定要對
image: 表情圖片的名字, 用來找到對應(yīng)的表情圖片
<2> 每一個表情包圖片都在一個文件夾中, 文件夾存在EmojiPackage.bundle目錄下, EmojiPackageList.plist文件也在改目錄下
<3> 根據(jù)plist文件創(chuàng)建對應(yīng)的數(shù)據(jù)模型
<4> 對于數(shù)據(jù)我們這里使用單例模式, 同時提供一個表情字符和表情圖片相互轉(zhuǎn)換的接口, 用于外部文字和字符串進(jìn)行互轉(zhuǎn)
真正的鍵盤
其實系統(tǒng)提供好接口給我們自定義鍵盤, 如下
// Presented when object becomes first responder. If set to nil, reverts to following responder chain. If
// set while first responder, will not take effect until reloadInputViews is called.
@property (nullable, readwrite, strong) UIView *inputView;
@property (nullable, readwrite, strong) UIView *inputAccessoryView;
UITextView和UITextField都有這兩個屬性, 所以我們只需要搭建好UI, 修改inputView屬性, 然后調(diào)用reloadInputViews就可以了, 這樣體驗就會和系統(tǒng)的一樣鍵盤一樣了, 如果需要按鍵音的話需要我們自定義的inputView類遵循UIInputViewAudioFeedback協(xié)議, 同時實現(xiàn) enableInputClicksWhenVisible方法并返回YES, 這樣就可以在點擊表情的時候調(diào)用[[UIDevice currentDevice] playInputClick]方法發(fā)出按鍵音了, Demo中沒有實現(xiàn), 需要的話可以自己添加
接下來就是搭建UI了
分析: 通常的表情鍵盤都分為三部分, 上方為可以左右滑動的表情視圖YBEmojiContentView, 中間為頁碼指示器UIPageControl, 最下方為表情包按鈕YBEmojiTabbar, 用來切換表情包, 接下來創(chuàng)建一個繼承自 UIView 的 YBEmojiInputView 作為inputView, 然后將上邊三部分分別添加到Y(jié)BEmojiInputView上, 然后實現(xiàn)具體的每一部分
YBEmojiInputView主要用來調(diào)節(jié)三個模塊的關(guān)系以及提供一些代理給外部使用, 外部只需要實現(xiàn)UITextView的代理方法, 對文字做對應(yīng)的處理就可以了, 代碼基本上是固定的, 這樣輸入框部分就不受限制了, 可以隨意定制自己的UI, 滿足不同用戶對UI的需求
@protocol YBEmojiInputViewDelegate<NSObject>
// 點擊表情
- (void)inputView:(YBEmojiInputView *)inputView clickedEmojiWith:(YBEmojiItemModel *)emoji;
// 點擊大表情
- (void)inputView:(YBEmojiInputView *)inputView clickedBigEmojiWith:(YBEmojiItemModel *)emoji;
// 點擊刪除
- (void)inputView:(YBEmojiInputView *)inputView clickedDeleteWith:(UIButton *)button;
// 點擊發(fā)送
- (void)inputView:(YBEmojiInputView *)inputView clickedSendWith:(UIButton *)button;
@end
指示器和底部欄比較簡單, 我們這里主要來說YBEmojiContentView
1. YBEmojiContentView
既然可以左右滑動, 那就不用考慮了, 先建一個UIScrollView, 至于contentSize是計算出來的, 具體怎么算用腳指頭想都知道
然后考慮到內(nèi)存, 我們不能一下創(chuàng)建那么按鈕上去, 所以我這邊的做法是創(chuàng)建一個YBEmojiPageView作為一頁, 然后創(chuàng)建三頁, 然后在scrollView滑動的時候來交換三個YBEmojiPageView的位置以及frame同時更新表情圖片即可
為了避免重復(fù)更新圖片消耗性能, 所以這邊創(chuàng)建一個pageFlag來記錄當(dāng)前頁碼, 當(dāng)前頁已經(jīng)更新了就不在更新了
核心代碼
// 更新三個pageView位置
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (_delegate && [_delegate respondsToSelector:@selector(contentView:didScrollViewToIndex:)]) {
NSInteger pageIndex = roundf(self.scrollView.contentOffset.x / self.bounds.size.width);
[_delegate contentView:self didScrollViewToIndex:pageIndex];
}
// 當(dāng)表情也小于等于兩頁的時候就不需要更新了
if (self.totalPage <= 2) { return; }
[self updatePagesView];
}
// 更新pageView位置以及內(nèi)容向右滑動后, 將最右邊的pageView放在最左邊, 重新賦值, 向左滑動也一樣>
- (void)updatePagesView {
CGFloat pageOffset = self.scrollView.contentOffset.x / self.bounds.size.width;
// 頁碼四舍五入取整
NSInteger page = roundf(pageOffset);
if (page != self.pageFlag) {
YBEmojiPageView *aView = nil;
if (pageOffset > page) { // 向右滑動
// 更新表情
[self.rightPageView configEmojisButtonWith:self.groupModel pageIndex:page - 1];
// 交換位置
aView = self.rightPageView;
self.rightPageView = self.centerPageView;
self.centerPageView = self.leftPageView;
self.leftPageView = aView;
}else { // 向左滑動
// 更新表情
[self.leftPageView configEmojisButtonWith:self.groupModel pageIndex:page + 1];
// 交換位置
aView = self.leftPageView;
self.leftPageView = self.centerPageView;
self.centerPageView = self.rightPageView;
self.rightPageView = aView;
}
// 更新pageViews的frame
[self layoutPageViewsWith:page];
}
self.pageFlag = page;
}
// 根據(jù)頁碼更新pageView的frame
- (void)layoutPageViewsWith:(NSInteger)page {
self.leftPageView.frame = CGRectMake((page - 1) * self.bounds.size.width, 0, self.bounds.size.width, self.bounds.size.height);
self.centerPageView.frame = CGRectMake(page * self.bounds.size.width, 0, self.bounds.size.width, self.bounds.size.height);
self.rightPageView.frame = CGRectMake((page + 1) * self.bounds.size.width, 0, self.bounds.size.width, self.bounds.size.height);
}
YBEmojiPageView作用是用來顯示表情, 主要功能如下:
- 計算一頁顯示最多表情數(shù)量, 然后將表情控件循環(huán)創(chuàng)建出來
- (instancetype)initWithConfig:(YBEmojiConfig *)config {
if (self = [super init]) {
self.config = config;
self.backgroundColor = config.pageViewBackgroundColor;
self.emojiItems = [NSMutableArray array];
// 初始化, 循環(huán)創(chuàng)建表情按鈕(每頁的按鈕數(shù)量 = 行數(shù) x 列數(shù), 最后一個為刪除按鈕, 所以表情按鈕數(shù)量要 -1<大表情就不需要-1了>)
NSInteger btnCount = MAX(config.smallEmojiLineCount*config.smallEmojiColumnCount-1, config.largeEmojiLineCount*config.largeEmojiColumnCount);
for (NSUInteger i = 0; i < btnCount; i++) {
YBEmojiItemView *emojiItem = [[YBEmojiItemView alloc] init];
[emojiItem addTarget:self action:@selector(clickedEmojiItemView:)];
[_emojiItems addObject:emojiItem];
[self addSubview:emojiItem];
}
// 刪除按鈕
self.deleteBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[self.deleteBtn setImage:self.config.pageViewDeleteButtonImage forState:UIControlStateNormal];
[self.deleteBtn addTarget:self action:@selector(clickedDeleteButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.deleteBtn];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressPageView:)];
longPress.minimumPressDuration = 0.25;
[self addGestureRecognizer:longPress];
}
return self;
}
- 提供給外界的接口用來更新該頁的表情
- (void)configEmojisButtonWith:(YBEmojiGroupModel *)groupModel pageIndex:(NSInteger)pageIndex {
self.isLargeEmoji = groupModel.isLargeEmoji;
// 重置表情按鈕圖片
NSArray<YBEmojiItemModel *> *emojis = [self emojiItemWith:groupModel atPageIndex:pageIndex];
self.hidden = emojis.count == 0;
for (int i = 0; i < self.emojiItems.count; i ++) {
YBEmojiItemView *emojiItemView = self.emojiItems[i];
// 設(shè)置表情圖片 當(dāng)表情數(shù)量不滿一整頁的時候, 其余按鈕圖片置空
YBEmojiItemModel *emoji = i < emojis.count ? emojis[i] : nil;
emojiItemView.isShowTitle = groupModel.isLargeEmoji;
[emojiItemView setEmoji:emoji];
}
[self setNeedsLayout];
}
// 獲取表情包對應(yīng)頁碼的模型數(shù)組
- (NSArray<YBEmojiItemModel *> *)emojiItemWith:(YBEmojiGroupModel *)groupModel atPageIndex:(NSInteger )pageIndex {
if (!groupModel || !groupModel.emojis.count) {
return nil;
}
NSInteger columnCount = self.isLargeEmoji ? self.config.largeEmojiColumnCount : self.config.smallEmojiColumnCount;
NSInteger lineCount = self.isLargeEmoji ? self.config.largeEmojiLineCount : self.config.smallEmojiLineCount;
NSInteger emojiCOuntOfPage = self.isLargeEmoji ? columnCount * lineCount : columnCount * lineCount - 1;
NSUInteger totalPage = (groupModel.emojis.count / emojiCOuntOfPage) + 1;
if (pageIndex >= totalPage || pageIndex < 0) {
return nil;
}
BOOL isLastPage = (pageIndex == totalPage - 1 ? YES : NO);
// 截取的初始位置
NSUInteger beginIndex = pageIndex * emojiCOuntOfPage;
// 截取長度
NSUInteger length = isLastPage ? (groupModel.emojis.count - pageIndex * emojiCOuntOfPage) : emojiCOuntOfPage;
NSArray *emojis = [groupModel.emojis subarrayWithRange:NSMakeRange(beginIndex, length)];
return emojis;
}
- 根據(jù)是否為大表情更新表情控件frame
- (void)layoutSubviews {
[super layoutSubviews];
NSInteger columnCount = self.isLargeEmoji ? self.config.largeEmojiColumnCount : self.config.smallEmojiColumnCount;
NSInteger lineCount = self.isLargeEmoji ? self.config.largeEmojiLineCount : self.config.smallEmojiLineCount;
// 計算表情按鈕寬度
CGFloat width = (self.bounds.size.width - self.config.pageViewEdgeInsets.left - self.config.pageViewEdgeInsets.right - ((columnCount - 1) * self.config.pageViewMinColumnSpace)) / (CGFloat)columnCount;
// 計算表情按鈕高度
CGFloat heigh = (self.bounds.size.height - self.config.pageViewEdgeInsets.top - self.config.pageViewEdgeInsets.bottom - ((lineCount - 1) * self.config.pageViewMinLineSpace)) / (CGFloat)lineCount;
// 表情按鈕為正方形, 所以取一個最小值作為寬高, 那么久需要重新計算行間距列間距
CGFloat minSize = MIN(width, heigh);
// 計算行間距
CGFloat lineSpace = (self.bounds.size.height - self.config.pageViewEdgeInsets.top - self.config.pageViewEdgeInsets.bottom - minSize * lineCount) / (CGFloat)(lineCount + 1);
// 計算列間距
CGFloat columnSpace = (self.bounds.size.width - self.config.pageViewEdgeInsets.left - self.config.pageViewEdgeInsets.right - minSize * columnCount) / (CGFloat)(columnCount + 1);
// 遍歷設(shè)置表情按鈕的frame
for (int i = 0; i < self.emojiItems.count; i ++) {
NSInteger line = i / columnCount; // 當(dāng)前行數(shù)
NSInteger column = i % columnCount; // 當(dāng)前列數(shù)
// 表情按鈕的最小 x 和最小 y
CGFloat minX = self.config.pageViewEdgeInsets.left + column * minSize + ((column + 1) * columnSpace);
CGFloat minY = self.config.pageViewEdgeInsets.top + (line * minSize) + ((line + 1) * lineSpace);
CGRect frame = CGRectMake(minX, minY, minSize, minSize);
self.emojiItems[i].frame = frame;
}
// 刪除按鈕
self.deleteBtn.frame = CGRectMake(self.bounds.size.width - self.config.pageViewEdgeInsets.right - minSize, self.bounds.size.height - self.config.pageViewEdgeInsets.bottom - minSize - lineSpace, minSize, minSize);
self.deleteBtn.hidden = self.isLargeEmoji;
}
- 長按手勢, 顯示預(yù)覽表情
- (void)longPressPageView:(UILongPressGestureRecognizer *)longPress {
YBEmojiItemView *emojiItemView = nil;
CGPoint point = [longPress locationInView:self];
// 遍歷當(dāng)前頁所有按鈕, 找到手指所在的按鈕
for (YBEmojiItemView *emojiItem in self.emojiItems) {
// 大表情長按時候有背景顏色, 小表情則沒有
if (CGRectContainsPoint(emojiItem.frame, point)) {
emojiItemView = emojiItem;
if (self.isLargeEmoji) {
// 大表情長按預(yù)覽的背景顏色
emojiItem.backgroundColor = self.config.largeEmojiHighlightBackgroundColor;
}else {
break;
}
}else {
emojiItem.backgroundColor = UIColor.clearColor;
}
}
if (longPress.state == UIGestureRecognizerStateFailed ||
longPress.state == UIGestureRecognizerStateCancelled ||
longPress.state == UIGestureRecognizerStateEnded ||
emojiItemView.emoji == nil) {
// hide preview
self.emojiPreview.hidden = YES;
// 清除在大表情的時候長按的背景顏色
if (self.isLargeEmoji) {
emojiItemView.backgroundColor = UIColor.clearColor;
}
}else {
// show preview
self.emojiPreview.hidden = NO;
UIWindow *window = [[[UIApplication sharedApplication] windows] lastObject];
// 先計算出相對于window的位置, 然后計算預(yù)覽視圖的frame
CGRect rectOfWindow = [emojiItemView convertRect:emojiItemView.bounds toView:window];
// 預(yù)覽視圖的寬度
CGFloat preview_w = self.isLargeEmoji ? self.config.largeEmojiPreviewSize.width : self.config.emojiPreviewSize.width;
// 預(yù)覽視圖的高度
CGFloat preview_h = self.isLargeEmoji ? self.config.largeEmojiPreviewSize.height : self.config.emojiPreviewSize.height;
// 預(yù)覽視圖的x
CGFloat preview_x = CGRectGetMaxX(rectOfWindow) - preview_w + (preview_w - rectOfWindow.size.width) / 2.0;
// 預(yù)覽視圖的y
CGFloat preview_y = self.isLargeEmoji ? CGRectGetMinY(rectOfWindow) - preview_h : CGRectGetMaxY(rectOfWindow) - preview_h;
// 計算大表情三角指示器的偏移量
CGFloat angleOffset_x = 0;
if (self.config.largeEmojiPreviewBorderMargin != 0 && self.isLargeEmoji) {
if (preview_x < self.config.largeEmojiPreviewBorderMargin) {
angleOffset_x = preview_x - self.config.largeEmojiPreviewBorderMargin;
preview_x = self.config.largeEmojiPreviewBorderMargin;
}
if (preview_x + preview_w > UIScreen.mainScreen.bounds.size.width - self.config.largeEmojiPreviewBorderMargin) {
angleOffset_x = self.config.largeEmojiPreviewBorderMargin + preview_x + preview_w - UIScreen.mainScreen.bounds.size.width;
preview_x = UIScreen.mainScreen.bounds.size.width - self.config.largeEmojiPreviewBorderMargin - preview_w;
}
}
CGRect frame = CGRectMake(preview_x, preview_y, preview_w, preview_h);
// 將當(dāng)前手指所在位置的表情模型給預(yù)覽視圖進(jìn)行顯示
[self.emojiPreview setEmojiItemModel:emojiItemView.emoji isLargeEmoji:self.isLargeEmoji];
self.emojiPreview.frame = frame;
// 用來調(diào)整大表情三角指示器的居中
[self.emojiPreview setAngleOffset:angleOffset_x];
}
}
- 提供代理給外界, 實現(xiàn)表情點擊以及刪除方法
@protocol YBEmojiPageViewDelegate<NSObject>
// 點擊表情
- (void)pageView:(YBEmojiPageView *)pageView clickedEmojiWith:(YBEmojiItemModel *)emoji;
// 點擊大表情
- (void)pageView:(YBEmojiPageView *)pageView clickedBigEmojiWith:(YBEmojiItemModel *)emoji;
// 點擊刪除
- (void)pageView:(YBEmojiPageView *)pageView clickedDeleteWith:(UIButton *)button;
@end
至于YBEmojiPreviewView是一個預(yù)覽視圖, 比較簡單, 只需要提供接口用來更改顯示的圖片, frame在外界根據(jù)當(dāng)前手指所在的按鈕來進(jìn)行計算, 在繪制一個大表情的背景線框就可以了, 具體請查看demo, 顯示gif的話, 代碼我拷貝的YLGifImage和YLGifImageView
2. UIPageControl 這個就不多說了, 不會的面壁去吧
3. YBEmojiTabbar作用是顯示表情包封面圖片, 以及提供點擊方法
底部無非就是若干個表情包按鈕和一個發(fā)送按鈕, UI以及代碼都比較簡單, 最后提供一個代理給外部使用就可以了
拷貝 粘貼 剪切
如果輸入框中顯示有表情圖片, 那么拷貝,剪切的時候, 我們需要將其創(chuàng)轉(zhuǎn)成字符串, 如果粘貼的時候文字中有表情字符串則需要轉(zhuǎn)換成表情圖片
所以Demo中提供了一個繼承自UITextView的YBEmojiTextView, 已經(jīng)實現(xiàn)拷貝粘貼剪切的功能, 使用者只需要繼承自該類即可, 具體的代碼實現(xiàn):
// 注: 以下代碼拷貝自 PPStickerKeyboard 也給我提供了一些思路, 還有其他一些相關(guān)代碼, 在此表示感謝
// 他的簡書地址http://www.itdecent.cn/p/9359b562a76f
- (void)cut:(id)sender {
NSString *string = [YBEmojiDataManager.manager plainStringWith:self.attributedText range:self.selectedRange];
if (string.length) {
[UIPasteboard generalPasteboard].string = string;
NSRange selectedRange = self.selectedRange;
NSMutableAttributedString *attributeContent = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
[attributeContent replaceCharactersInRange:self.selectedRange withString:@""];
self.attributedText = attributeContent;
self.selectedRange = NSMakeRange(selectedRange.location, 0);
if (self.delegate && [self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
[self.delegate textViewDidChange:self];
}
}
}
- (void)copy:(id)sender {
NSString *string = [YBEmojiDataManager.manager plainStringWith:self.attributedText range:self.selectedRange];
if (string.length) {
[UIPasteboard generalPasteboard].string = string;
}
}
- (void)paste:(id)sender {
NSString *string = UIPasteboard.generalPasteboard.string;
if (string.length) {
NSMutableAttributedString *attributedPasteString = [[NSMutableAttributedString alloc] initWithString:string];
attributedPasteString = [YBEmojiDataManager.manager replaceEmojiWithAttributedString:attributedPasteString attributes:self.attributes];
NSRange selectedRange = self.selectedRange;
NSMutableAttributedString *attributeContent = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
[attributeContent replaceCharactersInRange:self.selectedRange withAttributedString:attributedPasteString];
self.attributedText = attributeContent;
self.selectedRange = NSMakeRange(selectedRange.location + attributedPasteString.length, 0);
if (self.delegate && [self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
[self.delegate textViewDidChange:self];
}
}
}
以上就是實現(xiàn)表情鍵盤的一些思路以及要點, 具體請查看Demo, 代碼已上傳至Github
解決在ios14上圖片顯示不出來的bug
// 在YBEmojiGifImageView中對displayLayer方法做如下修改
- (void)displayLayer:(CALayer *)layer
{
if (!self.animatedImage || [self.animatedImage.images count] == 0) {
if (@available(iOS 14.0, *)) {
[super displayLayer:layer];
}
return;
}
//NSLog(@"display index: %luu", (unsigned long)self.currentFrameIndex);
if(self.currentFrame && ![self.currentFrame isKindOfClass:[NSNull class]]) {
layer.contents = (__bridge id)([self.currentFrame CGImage]);
}
}
注: 暫不支持YYText