今天,我終於更更更更博了。 接着上一篇聊天界面從0到1的實現 (一), 今天來聊一聊 聊天頁面的底部橫條。ios
原文地址 : 聊天界面從0到1的實現 (二)git
demo 地址: JPChatBottomBargithub
JPChatBottomBar
與如今主流的聊天頁面的底部橫條頁面類似。 相似於微信中的:正則表達式
之因此先從這個橫條來折騰,我的想法:從功能上來講,這個模塊能夠從Im中獨立出來,但又能夠屏蔽掉因通訊部分第三方服務選擇的不一樣而帶來的差別,服務於聊天的整個框架。之後若是框架發生變化,這一模塊受到的影響也會是最小的。算法
JPChatBottomBar
雖然並不整個框架的核心,但卻也提供着基礎的服務功能——編輯消息。 本身在模仿實現一個橫條的過程當中,也遇到了一些麻煩。windows
礙於篇幅,文章中主要用於記敘一些比較複雜的實現抑或是一些細節的問題,簡單的邏輯判斷實現就不出如今這裏了。數組
demo的地址放在這裏:JPChatBottomBar--github地址bash
結合前面的圖:能夠初步總結出 JPChatBottomBar
應該實現的功能,以下:服務器
這裏,咱們經過 一個代理 JPChatBottomBarDelegate
來將用戶的操做(文本消息、語音消息等等)向外傳遞,即向聊天框架中的其餘模塊提供服務。微信
先來對我所使用到的類來進行說明:
JPChatBottomBar
: 整個橫條preview
文件中的類用於實現表情包的預覽效果imageResource
文件夾中存放了此demo中所用到的圖片資源JPEmojiManager
:這個類用於讀取全部的表情包資源JPPlayerHelper
:這個類用於實現錄音和播音的效果JPAttributedStringHelper
:實現表情包子符和表情包圖片的互轉model
,JPEmojiModel
用於綁定單獨一個表情包,JPEmojiGroupModel
用於綁定一整組的表情包。category
中存放了一些經常使用的工具類下面,讓我就上面所羅列的應該實現的功能,來說講各功能我是如何實現或者是在實現的過程當中我所遇到的問題。
效果能夠到個人博客或者下載demo中查看。
能夠看到,在鍵盤彈出的過程當中,controller.view
要向上滑動,避免彈出的鍵盤遮擋住了用戶的聊天頁面。這也是很是基礎的功能。
可是這裏有個細節的地方:
這一塊一開始我是想經過寫死系統鍵盤的高度,經過監聽textView.inputView
新舊值的變化(kvo實現參考demo裏面): 從demo中的代碼能夠看出,在等到chatBottomBar
到達了該到的位置以後,再調用-textView reloadInputView
來喚醒鍵盤。如此就能夠達到鍵盤從下彈出而且不會有小部分覆蓋的效果。
可是發現,系統鍵盤的高度不是都同樣的,例如漢語拼音的九宮格鍵盤要比26鍵高,而日語九宮格鍵盤要比26鍵低,因此不是很全。
因而最後仍是採用了監聽鍵盤彈出的通知來實現 :
而微信在這一塊的實現就沒有這種覆蓋效果,微信等到viewController.view
來到該到的位置以後,再讓鍵盤從下面彈出。
監聽鍵盤彈出的通知:
// JPChatBottomBar.m // 監聽textView.inputView屬性新舊值的變化 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeRect:) name:UIKeyboardWillChangeFrameNotification object:nil]; - (void)keyboardWillChangeRect:(NSNotification *)noti { NSValue * aValue = noti.userInfo[UIKeyboardFrameBeginUserInfoKey]; self.oldRect = [aValue CGRectValue]; NSValue * newValue = noti.userInfo[UIKeyboardFrameEndUserInfoKey]; self.newRect = [newValue CGRectValue]; [UIView animateWithDuration:0.3 animations:^{ if(self.superview.y == 0) { self.superview.y -= self.newRect.size.height; }else { self.superview.y -=(self.newRect.size.height - self.oldRect.size.height); } } completion:^(BOOL finished) { }]; } 複製代碼
路過的讀者若是有更好的改進方法,能在切換鍵盤的時候避免這種覆蓋,歡迎提出,我也是正在學習iOS 的小白😂。謝謝🙏🙏🙏
關於鍵盤的切換剩下的就是 根據用戶的點擊切換鍵盤的狀態(變化相應的視圖)。 這一部分就先到此☺️👌🏾。
在參考了別人的Demo(iOS仿微信錄音控件Demo)以後,我也實現了一個。
先給出本身所使用的類的介紹:
JPPlayerHelper
: 實現錄音和播音的功能。JPAudioView
: 展現錄音的狀態讓我歸納一下 實現的大概步驟。 首先兩個類之間並非相互依賴的,二者在JPChatBottomBar
中產生耦合。
我在JPAudioView中利用了下面着三個方法來讓audioView對用戶手勢變化進行判斷(開始點擊、向上向下滑動、手指離開),並做出相應的處理,代碼以下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event ;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
複製代碼
在上面這三個方面中解決自身的UI問題,再經過block從外部來實現錄音以及根據語音強度更新UI的效果:
/// AudioView block塊的實現(JPChatBottomBar.m) - (JPAudioView *)audioView { if(!_audioView) { JPAudioView * tmpView = [[JPAudioView alloc] initWithFrame:CGRectMake(self.textView.x, self.textView.y, self.textView.width, _btnWH )]; [self addSubview:tmpView]; _audioView = tmpView; // 實現audioView的方法 __weak typeof (self) wSelf = self; _audioView.pressBegin = ^{ [wSelf.audioView setAudioingImage:[UIImage imageNamed:@"zhengzaiyuyin_1"] text:@"鬆開手指,上滑取消"]; // 開始錄音 [wSelf.recoder jp_recorderStart]; }; _audioView.pressingUp = ^{ [wSelf.audioView setAudioingImage:[UIImage imageNamed:@"songkai"] text:@"鬆開手指,取消發送"]; }; _audioView.pressingDown = ^{ NSString * imgStr = [NSString stringWithFormat:@"zhengzaiyuyin_%d",imageIndex]; [wSelf.audioView setAudioingImage:[UIImage imageNamed:imgStr] text:@"鬆開發送,上滑取消"]; }; _audioView.pressEnd = ^{ [wSelf.audioView setAudioViewHidden]; [wSelf.recoder jp_recorderStop]; NSString * filePath = [wSelf.recoder getfilePath]; NSData * audioData = [NSData dataWithContentsOfFile:filePath]; /// 將語音消息data經過代理向外傳遞 if(wSelf.agent && [wSelf.agent respondsToSelector:@selector(msgEditAgentAudio:)]){ [wSelf.agent msgEditAgentAudio:audioData]; } if(wSelf.msgEditAgentAudioBlock){ wSelf.msgEditAgentAudioBlock(audioData); } }; } return _audioView; } 複製代碼
其次就是這一塊比較關鍵的點: 根據語音的強度來刷新audioView的UI
效果能夠到博客或者下載demo查看。
咱們首先獲取語音強度平均值的方法主要經過:
/// 更新測量值 - (void)updateMeters; /* call to refresh meter values */ /// 獲取峯值 - (float)peakPowerForChannel:(NSUInteger)channelNumber; /// 獲取平均值 - (float)averagePowerForChannel:(NSUInteger)channelNumber; 複製代碼
在獲取語音強度的時候,須要先updateMeters
更新一下測量值。 而後咱們能夠經過測量值 、 峯峯值以後,根據必定的算法來計算出此時聲音的相對大小強度。這裏,算法很垃圾的我簡單的設計了一個:
// JPPlayerHelper.m - (CGFloat)audioPower { [self.recorder updateMeters]; // 更新測量值 float power = [self.recorder averagePowerForChannel:0]; // 平均值 取得第一個通道的音頻,注意音頻的強度爲[-160,0],0最大 // float powerMax = [self.recorder peakPowerForChannel:0]; // CGFloat progress = (1.0/160.0) * (power + 160); power = power + 160 - 50; int dB = 0; if (power < 0.f) { dB = 0; } else if (power < 40.f) { dB = (int)(power * 0.875); } else if (power < 100.f) { dB = (int)(power - 15); } else if (power < 110.f) { dB = (int)(power * 2.5 - 165); } else { dB = 110; } return dB; } 複製代碼
關於這一塊的算法,若是各位讀者有更好的方法,歡迎提出,我也是個渴望知識的小白。
經過上面的方法能夠獲取相應的聲音的分貝強度,咱們外部能夠作一些處理:例如我作了,當新測量值比舊的測量值大必定值的時候,就作提升分貝的UI刷新操做,低的時候就作下降分貝UI的操做,具體能夠看下面的代碼:
// JPChatbottomBar.m - (void) jpHelperRecorderStuffWhenRecordWithAudioPower:(CGFloat)power{ NSLog(@"%f",power); NSString * newPowerStr =[NSString stringWithFormat:@"%f",[self.helper audioPower]]; if([newPowerStr floatValue] > [self.audioPowerStr floatValue]) { if(imageIndex == 6){ return; } imageIndex ++; }else { if(imageIndex == 1){ return; } imageIndex --; } if(self.audioView.state == JPPressingStateUp) { self.audioView.pressingDown(); } self.audioPowerStr = newPowerStr;; } 複製代碼
其次,我在JPPlayerHepler
加了一個計時器來觸發反覆調用上面的代理方法(- (void) jpHelperRecorderStuffWhenRecordWithAudioPower:(CGFloat)power
) ,讓其能夠進行UI的刷新,由於若是不加計時器,咱們是沒有事件去觸發audioView
UI刷新的操做,計時器相關方法以下:
// JPPlayerHelper.m -(NSTimer *)timer{ if (!_timer) { _timer=[NSTimer scheduledTimerWithTimeInterval:0.35 target:self selector:@selector(doOutsideStuff) userInfo:nil repeats:YES]; } return _timer; } - (void)doOutsideStuff { if(self.delegate && [self.delegate respondsToSelector:@selector(jpHelperRecorderStuffWhenRecordWithAudioPower:)]){ [self.delegate jpHelperRecorderStuffWhenRecordWithAudioPower:[self audioPower]]; } } 複製代碼
完成錄音以後,最終咱們的語音數據經過JPChatBottomBarDelegate
的代理方法向外提供。
關於獲取語音強度那一塊的算法並非最優,我以爲個人算法也是比較笨拙存在缺點(對用戶語音強度的變化不敏感)。若是路過的讀者有什麼不錯的建議,歡迎提出補充,我也會採納,謝謝🙏🙏🙏。
JPChatBottomBar
裏面的‘更多’鍵盤與微信的相似。
開發者在使用的時候若是想要鍵入不一樣的功能實現,只要在/ImageResource/JPMoreBundle.bundle
的JPMorePackageList.plist
文件中添加相應的item
內部也已經作好了適配的效果,不過當item數量超過8個時候,沒有完成像微信的那種分頁效果,後期我會繼續完善。
當用戶點擊了上面的某個item以後,咱們就將事件經過JPChatBottomBarDelegate
向外面傳遞,開發者能夠再最外層作處理,根據點擊哪一個item響應相應的方法功能,相似以下代碼:
// ViewController.m NSString * kJPDictKeyImageStrKey = @"imageStr"; - (void)msgEditAgentClickMoreIVItem:(NSDictionary *)dict { NSString * judgeStr = dict[kJPDictKeyImageStrKey]; if([judgeStr isEqualToString:@"photo"]){ NSLog(@"點擊了圖冊"); }else if([judgeStr isEqualToString:@"camera"]){ NSLog(@"點擊了攝像頭"); }else if([judgeStr isEqualToString:@"file"]) { NSLog(@"點擊了文件"); }else if([judgeStr isEqualToString:@"location"]) { NSLog(@"點擊了位置"); } } 複製代碼
一開始沒有想着將用戶點擊哪一個item暴露在外面,但後來想了開發者面臨的業務多種多樣,爲了更好的擴展,簡化JPChatBottomBar
的結構,就將這部分也經過代理寫出來。
我花了比較多的時間在這一部分上面,以前沒有真正的作嵌入表情包的方法,只是經過調用原生的表情來實現表情的編輯
這一部分主要思考 當用戶點擊表情包的時候咱們要作哪些處理。 先講咱們的問題化簡一下。
觀察微信,表情包主要分兩大種,一種是能夠嵌入文本框的表情,而另外一種是當用戶點擊了該表情以後直接就發送給聊天對象,下面咱們稱這兩種表情分別爲SmallEmoji(前者)和LargeEmoji(後者)。
後者的實現方式能夠經過每一層間代理將其暴露在外。
// JPChatBottomBar.h
/**
* 用戶點擊了鍵盤的表情包按鈕
* @param bigEmojiData : 大表情包的data
*/
- (void)msgEditAgentSendBigEmoji:(NSData *)bigEmojiData;
複製代碼
關於「點擊SmallEmoji嵌入文本」,我放後談談。
這裏我經過JPEmojiManager
將表情包從/ImageResource/JPEmojiBundle.bundle
中加載出來,爲一個表情包在JPEmojiPackageList.plist
中都有對應的item進行綁定,所以,若是後期咱們有新的表情包,只要把圖片存進去,而且在plist文件中增長新的item便可以,代碼實現用戶動態添加表情包的方式也是同樣的。
而爲了不重複地讀取文件,我將JPEmojiManager
寫成了單例。
// JPEmojiManager.h /** * @return 獲取全部的表情組 */ - (NSArray <JPEmojiGroupModel *> *)getEmogiGroupArr; /** * 根據位置獲取相應的模型數組 * @param group : 選擇了哪一組表情 * @param page : 頁碼 * @return 根據前面兩個參數從全部數據中根據對應的位置和大小取出表情模型(<= 20個) */ - (NSArray <JPEmojiModel *> *)getEmojiArrGroup:(NSInteger)group page:(NSInteger)page; 複製代碼
這裏能夠看到兩個類
JPEmojiModel
用於綁定單獨一個表情包,JPEmojiGroupModel
用於綁定一整組的表情包JPEmojiManager
中的這兩個方法更可能是服務於分頁表情包的效果(下面我將要談到)
在看過github上面別人表情包demo以後,有一些並無實現滑動切換表情包組,因而本身實現了一個,效果能夠到博客或者demo查看。
分也效果的實現方式:經過三個view去複用,在ScrollView
中去輪流展現。
// JPEmojiInputView.m #pragma mark 三個view複用 @property (strong, nonatomic) JPInputPageView * leftPV; @property (strong, nonatomic) JPInputPageView * currentPV; @property (strong, nonatomic) JPInputPageView * rightPV; 複製代碼
經過前面JPEmojiManager
中取出對應頁數的表情包以後,而後調用下面的方法講每一頁的表情包傳入每個分頁
// JPInputPageView.h /** * 賦予新的數組,從新刷新數據源 * @param emojiArr : 一頁的表情(做爲內置CollectionView的數據 */ - (void)setEmojiArr:(NSArray <JPEmojiModel *> *)emojiArr isShowLargeImage:(BOOL)value; 複製代碼
先來說講三個分頁實現展現全部表情的效果:
self.currentPV
。leftPv
移動到了最右邊,同時去除該頁的表情包,作好展現的準備。完成這一步以後,就是更換杯子中的水的問題了,將三個複用view的相互賦值:// JPEmojiInputView.m
JPInputPageView * tmpView ;
tmpView = self.leftPV;
self.leftPV = self.currentPV;
self.currentPV = self.rightPV;
self.rightPV = tmpView;
複製代碼
我經過下面的圖片來展現這一塊底層的實現,可能能夠方便你們理解:
當用戶手指向右滑展現上一頁的時候,底部實現的方式也是相似,以此類推。 更多細節(如何計算當前頁對應哪一組表情包的哪一頁等)能夠參考我寫在JPEmojiInputView.m
裏的- (void)scrollViewDidScroll:(UIScrollView *)scrollView
。
iOS 中textView和textField能夠自動識別系統原生的表情:
而針對咱們開發者另外添加的小表情,textView和textField不能直接識別。
這裏能夠參考了幾個主流app的實現方式,
而要注意的是,當咱們將‘圖文混編’的文本消息發送出去通過咱們服務器的時候,通常是不對字符串中的圖片信息進行解析,所以,底層依舊是向服務器傳遞純文本消息,而對裏面的圖片信息作了處理轉換成了表情包的描述文本,下面我用一張圖片解釋這個問題:
能夠看到咱們本地須要對這些「圖文混編」的文本轉換成純文本才能發送至服務端。這裏主要經過兩個系統的類來進行表情包和其描述文本的匹配。
NSTextAttachment
: 文本中的‘插件’,咱們經過這個類來插入圖片。NSRegularExpression
: 使用正則表達式來匹配字符串中的表情包描述文本。關於正則表達式,這裏有一篇比較全的語法:正則表達式。
這裏個人正則匹配的字符以下:
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\[.+?\\]" options:0 error:NULL];
在匹配出每個表情包描述文本以後,會生成一個數組存放這些匹配結果(描述文本、圖片資源、描述文本在原字符串的位置),而後遍歷這個數組,將這些描述文本經過插入圖片插件textAttachment
來替換,這裏注意,每次替換,後面尚未被替換的表情包文本的range就會發送變化,咱們須要遞減他們原來的位置range.location
。 具體實現的方式可參考下面代碼:
// JPAttributedStringHelper.m - (NSAttributedString *)getTextViewArrtibuteFromStr:(NSString *)str { if(str.length == 0) { return nil; } NSMutableAttributedString * attStr = [[NSMutableAttributedString alloc] initWithString:str attributes:[JPAttributedStringConfig getAttDict]]; NSMutableParagraphStyle * paraStyle = [[NSMutableParagraphStyle alloc] init]; paraStyle.lineSpacing = 5; [attStr addAttribute:NSParagraphStyleAttributeName value:paraStyle range:NSMakeRange(0, attStr.length)]; NSArray<JPEmojiMatchingResult *> * emojiStrArr = [self analysisStrWithStr:str]; if(emojiStrArr && emojiStrArr.count != 0) { NSInteger offset = 0; // 表情包文本的偏移量 for(JPEmojiMatchingResult * result in emojiStrArr ){ if(result.emojiImage ){ // 表情的特殊字符 NSMutableAttributedString * emojiAttStr = [[NSMutableAttributedString alloc] initWithAttributedString:[NSAttributedString attributedStringWithAttachment:result.textAttachment]]; if(!emojiAttStr) { continue; } NSRange actualRange = NSMakeRange(result.range.location - offset, result.range.length); [attStr replaceCharactersInRange:actualRange withAttributedString:emojiAttStr]; // 一個表情佔一個長度 offset += (result.range.length-1); } } return attStr; }else { return [[NSAttributedString alloc] initWithString:str attributes:[JPAttributedStringConfig getAttDict]];; } } 複製代碼
實現的效果能夠到博客或者下載demo查看。
而在按下刪除鍵,要實現刪除表情包描述文本,咱們須要判斷textView.selectedRange
所在的位置是否爲表情描述文本,代碼參考以下:
// 點擊了文本消息和或者表情包鍵盤的刪除按鈕 - (void)clickDeleteBtnInputView:(JPEmojiInputView *)inputView { NSString * souceText = [self.textView.text substringToIndex:self.textView.selectedRange.location]; if(souceText.length == 0) { return; } NSRange range = self.textView.selectedRange; if(range.location == NSNotFound) { range.location = self.textView.text.length; } if(range.length > 0) { [self.textView deleteBackward]; return; }else { // 正則表達式匹配要替換的文字的範圍 if([souceText hasSuffix:@"]"]){ // 表示該選取字段最後一個是表情包 if([[souceText substringWithRange:NSMakeRange(souceText.length-2, 1)] isEqualToString:@"]"]) { // 表示這只是一個單獨的字符@"]" [self.textView deleteBackward]; return; } // 正則表達式 NSString * pattern = @"\\[[a-zA-Z0-9\\u4e00-\\u9fa5]+\\]"; NSError *error = nil; NSRegularExpression * re = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];if (!re) {NSLog(@"%@", [error localizedDescription]);} NSArray *resultArray = [re matchesInString:souceText options:0 range:NSMakeRange(0, souceText.length)]; if(resultArray.count != 0) { /// 表情最後一段存在表情包字符串 NSTextCheckingResult *checkingResult = resultArray.lastObject; NSString * resultStr = [souceText substringWithRange:NSMakeRange(0, souceText.length - checkingResult.range.length)]; self.textView.text = [self.textView.text stringByReplacingCharactersInRange:NSMakeRange(0, souceText.length) withString:resultStr]; self.textView.selectedRange = NSMakeRange(resultStr.length , 0); }else { [self.textView deleteBackward]; } }else { // 表示最後一個不是表情包 [self.textView deleteBackward]; } } // textView自適應 [self textViewDidChange:self.textView]; } 複製代碼
實現效果你們能夠看看demo🤓。
看到這裏,我已經寫了近5k字了😂
表情包的預覽效果分爲
前者的底部視圖是一張已經畫好的圖片,
後者的底部視圖我經過重繪機制(QuartzCore
框架而且重寫-drawRect:
方法)來進行描邊以及視圖顏色的填充(關於重繪製:iOS開發之drawRect的做用和調用機制),代碼:
// JPGIfPreview.m
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//1.添加繪圖路徑
CGContextMoveToPoint(context,0,_filletRadius);
CGContextAddLineToPoint(context, 0, _squareHeight - _filletRadius);
CGContextAddQuadCurveToPoint(context, 0, _squareHeight ,_filletRadius, _squareHeight);
CGContextAddLineToPoint(context, (_squareWidht - _triangleWdith )/2,_squareHeight);
CGContextAddLineToPoint(context,BaseWidth /2 , BaseHeight);
CGContextAddLineToPoint(context, (_squareWidht + _triangleWdith )/2,_squareHeight);
CGContextAddLineToPoint(context, _squareWidht - _filletRadius,_squareHeight);
CGContextAddQuadCurveToPoint(context, _squareWidht, _squareHeight ,_squareWidht, _squareHeight - _filletRadius);
CGContextAddLineToPoint(context, _squareWidht ,_filletRadius);
CGContextAddQuadCurveToPoint(context, _squareWidht, 0 ,_squareWidht - _filletRadius, 0);
CGContextAddLineToPoint(context,_filletRadius ,0);
CGContextAddQuadCurveToPoint(context, 0, 0 ,0, _filletRadius);
//2.設置顏色屬性
CGFloat backColor[4] = {1,1,1, 0.86};
CGFloat layerColor[4] = {0.9,0.9,0.9,0};
//3.設置描邊顏色,填充顏色
CGContextSetFillColor(context, backColor);
CGContextSetStrokeColor(context, layerColor);
//4.繪圖
CGContextDrawPath(context, kCGPathFillStroke);
}
複製代碼
在完成了佈局以後,接下來就是要將咱們的預覽視圖添加到界面上。
在collectionView上面添加長按收拾longPress
,監聽手勢的狀態,而且計算手勢所在位置對應的cell,對其內容進行預覽效果的展現。
這裏我選擇了[UIApplication sharedApplication].windows.lastobject
做爲superView,即emojiInputView
所在的window。
這裏有個要注意的點,上面的windowCGPointZero
是從手機左上角開始算起,所以換算座標(cell是每個表情包,補充一下我是用collectionView來展現每個分頁上的表情包)時,我將cell.frame轉換成了在window上的frame:
CGRect rect = [[UIApplication sharedApplication].windows.lastObject convertRect:cell.frame fromView:self.collectionView];
複製代碼
座標換算完成以後,剩下的就是添加上去。
gif的播放效果,我也是第一次接觸,這裏看到一篇不錯的文章:iOS-Gif圖片展現N種方式(原生+第三方),裏面有介紹原生和第三方的實現。 考慮到減小項目的依賴庫,這裏我就採用了裏面原生方式的代碼,具體能夠點開連接看內部代碼,這裏不作過多敘述了(5.3k字了😂😂😂)。
這些文章對我提供了必定的幫助,也但願對你有用
WWDC 2017 - 優化輸入體驗的關鍵:keyboard技巧全介紹
在完成JPChatBottomBar
以後,整個框架訪問用戶編輯的消息或者是用戶的其餘操做均可以經過JPChatBottomBarDelegate
獲取。
JPChatBottomBar
部分就先到這裏,完成這部份內容,從demo到文章落筆完成之間,遇到了挺多問題😂。 例如‘切換鍵盤覆蓋的問題’那一塊本身就用了兩種方法來實現,亦或者是‘表情包組別的切換’,本身都花了有些時間。
針對個人文章和demo中技術的實現,若是讀者有更好的方法,歡迎提出🙏,謝謝🙏。
也但願個人文章可以給你帶來幫助。
若是對你有所幫助,請給我個Star吧✨。謝謝!