// // XHMessageTableViewController.m // MessageDisplayExample // // Created by HUAJIE-1 on 14-4-24. // Copyright (c) 2014年 曾宪华 开发团队(http://iyilunba.com ) 本人QQ:543413507 本人QQ群(142557668). All rights reserved. // #import "XHMessageTableViewController.h" #import #import "XHAudioPlayerHelper.h" #import "JSToastDialogs.h" @interface XHMessageTableViewController () { CGFloat _defInputFrameHeight; // 默认的输入工具条的高度 } /** * 判断是否用户手指滚动 */ @property (nonatomic, assign) BOOL isUserScrolling; /** * 记录旧的textView contentSize Heigth */ @property (nonatomic, assign) CGFloat previousTextViewContentHeight; /** * 记录键盘的高度,为了适配iPad和iPhone */ @property (nonatomic, assign) CGFloat keyboardViewHeight; @property (nonatomic, assign, readwrite) XHInputViewType textViewInputViewType; @property (nonatomic, weak, readwrite) XHMessageTableView *messageTableView; @property (nonatomic, weak, readwrite) XHMessageInputView *messageInputView; @property (nonatomic, weak, readwrite) XHShareMenuView *shareMenuView; @property (nonatomic, weak, readwrite) XHEmotionManagerView *emotionManagerView; @property (nonatomic, strong, readwrite) XHVoiceRecordHUD *voiceRecordHUD; @property (nonatomic, strong) UIView *headerContainerView; @property (nonatomic, strong) UIActivityIndicatorView *loadMoreActivityIndicatorView; /** * 管理地理位置的工具对象 */ @property (nonatomic, strong) XHLocationHelper *locationHelper; /** * 判断是不是超出了录音最大时长 */ @property (nonatomic) BOOL isMaxTimeStop; #pragma mark - DataSource Change /** * 改变数据源需要的子线程 * * @param queue 子线程执行完成的回调block */ - (void)exChangeMessageDataSourceQueue:(void (^)())queue; /** * 执行块代码在主线程 * * @param queue 主线程执行完成回调block */ - (void)exMainQueue:(void (^)())queue; #pragma mark - Previte Method /** * 判断是否允许滚动 * * @return 返回判断结果 */ - (BOOL)shouldAllowScroll; #pragma mark - Life Cycle /** * 配置默认参数 */ - (void)setup; /** * 初始化显示控件 */ - (void)initilzer; #pragma mark - RecorderPath Helper Method /** * 获取录音的路径 * * @return 返回录音的路径 */ - (NSString *)getRecorderPath; #pragma mark - UITextView Helper Method /** * 获取某个UITextView对象的content高度 * * @param textView 被获取的textView对象 * * @return 返回高度 */ - (CGFloat)getTextViewContentH:(UITextView *)textView; #pragma mark - Layout Message Input View Helper Method /** * 动态改变TextView的高度 * * @param textView 被改变的textView对象 */ - (void)layoutAndAnimateMessageInputTextView:(UITextView *)textView; #pragma mark - Scroll Message TableView Helper Method /** * 根据bottom的数值配置消息列表的内部布局变化 * * @param bottom 底部的空缺高度 */ - (void)setTableViewInsetsWithBottomValue:(CGFloat)bottom; /** * 根据底部高度获取UIEdgeInsets常量 * * @param bottom 底部高度 * * @return 返回UIEdgeInsets常量 */ - (UIEdgeInsets)tableViewInsetsWithBottomValue:(CGFloat)bottom; #pragma mark - Message Calculate Cell Height /** * 统一计算Cell的高度方法 * * @param message 被计算目标消息对象 * @param indexPath 被计算目标消息所在的位置 * * @return 返回计算的高度 */ - (CGFloat)calculateCellHeightWithMessage:(XHMessage *)message atIndexPath:(NSIndexPath *)indexPath; #pragma mark - Message Send helper Method /** * 根据文本开始发送文本消息 * * @param text 目标文本 */ - (void)didSendMessageWithText:(NSString *)text; /** * 根据图片开始发送图片消息 * * @param photo 目标图片 */ - (void)didSendMessageWithPhoto:(UIImage *)photo; /** * 根据视频的封面和视频的路径开始发送视频消息 * * @param videoConverPhoto 目标视频的封面图 * @param videoPath 目标视频的路径 */ - (void)didSendMessageWithVideoConverPhoto:(UIImage *)videoConverPhoto videoPath:(NSString *)videoPath; /** * 根据录音路径开始发送语音消息 * * @param voicePath 目标语音路径 * @param voiceDuration 目标语音时长 */ - (void)didSendMessageWithVoice:(NSString *)voicePath voiceDuration:(NSNumber*)voiceDuration; /** * 根据第三方gif表情路径开始发送表情消息 * * @param emotionPath 目标gif表情路径 */ - (void)didSendEmotionMessageWithEmotionPath:(NSString *)emotionPath; /** * 根据地理位置信息和地理经纬度开始发送地理位置消息 * * @param geolcations 目标地理信息 * @param location 目标地理经纬度 */ - (void)didSendGeolocationsMessageWithGeolocaltions:(NSString *)geolcations location:(CLLocation *)location; #pragma mark - Voice Recording Helper Method /** * 开始录音 */ - (void)startRecord; /** * 完成录音 */ - (void)finishRecorded; /** * 想停止录音 */ - (void)pauseRecord; /** * 继续录音 */ - (void)resumeRecord; /** * 取消录音 */ - (void)cancelRecord; @end @implementation XHMessageTableViewController #pragma mark - DataSource Change - (void)exChangeMessageDataSourceQueue:(void (^)())queue { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), queue); } - (void)exMainQueue:(void (^)())queue { dispatch_async(dispatch_get_main_queue(), queue); } - (void)addMessage:(XHMessage *)addedMessage { WEAKSELF [self exChangeMessageDataSourceQueue:^{ NSMutableArray *messages = [NSMutableArray arrayWithArray:[weakSelf getMessagesToShow]]; [messages addObject:addedMessage]; NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:1]; [indexPaths addObject:[NSIndexPath indexPathForRow:messages.count - 1 inSection:0]]; [weakSelf exMainQueue:^{ weakSelf.messages = messages; [weakSelf.messageTableView reloadData]; // [weakSelf.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; [weakSelf scrollToBottomAnimated:YES]; }]; }]; } - (void)removeMessageAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row >= self.messages.count) return; [self.messages removeObjectAtIndex:indexPath.row]; NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:1]; [indexPaths addObject:indexPath]; [self.messageTableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationBottom]; } static CGPoint delayOffset = {0.0}; // http://stackoverflow.com/a/11602040 Keep UITableView static when inserting rows at the top - (void)insertOldMessages:(NSArray *)oldMessages completion:(void (^)())completion { WEAKSELF [self exChangeMessageDataSourceQueue:^{ delayOffset = weakSelf.messageTableView.contentOffset; NSMutableArray *indexPaths = [[NSMutableArray alloc] initWithCapacity:oldMessages.count]; NSMutableIndexSet *indexSets = [[NSMutableIndexSet alloc] init]; [oldMessages enumerateObjectsUsingBlock:^(XHMessage *obj, NSUInteger idx, BOOL *stop) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:idx inSection:0]; [indexPaths addObject:indexPath]; delayOffset.y += [weakSelf calculateCellHeightWithMessage:[oldMessages objectAtIndex:idx] atIndexPath:indexPath]; [indexSets addIndex:idx]; }]; NSMutableArray *messages = [[NSMutableArray alloc] initWithArray:weakSelf.messages]; [messages insertObjects:oldMessages atIndexes:indexSets]; [weakSelf exMainQueue:^{ [UIView setAnimationsEnabled:NO]; weakSelf.messageTableView.userInteractionEnabled = NO; //[self.messageTableView beginUpdates]; weakSelf.messages = messages; [weakSelf.messageTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; //[self.messageTableView endUpdates]; [UIView setAnimationsEnabled:YES]; [weakSelf.messageTableView setContentOffset:delayOffset animated:NO]; weakSelf.messageTableView.userInteractionEnabled = YES; if (completion) { completion(); } }]; }]; } - (void)insertOldMessages:(NSArray *)oldMessages { [self insertOldMessages:oldMessages completion:nil]; } #pragma mark - Propertys - (NSMutableArray *)getMessagesToShow{ NSMutableArray *arr = [NSMutableArray arrayWithArray:_messages]; [_messages enumerateObjectsUsingBlock:^(XHMessage *obj, NSUInteger idx, BOOL * _Nonnull stop) { if (obj.hide) { [arr removeObject:obj]; } }]; return arr; } - (NSMutableArray *)messages { if (!_messages) { _messages = [[NSMutableArray alloc] initWithCapacity:0]; } return _messages; } - (UIView *)headerContainerView { if (!_headerContainerView) { _headerContainerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 8)]; _headerContainerView.backgroundColor = self.messageTableView.backgroundColor; [_headerContainerView addSubview:self.loadMoreActivityIndicatorView]; } return _headerContainerView; } - (UIActivityIndicatorView *)loadMoreActivityIndicatorView { if (!_loadMoreActivityIndicatorView) { _loadMoreActivityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; _loadMoreActivityIndicatorView.center = CGPointMake(CGRectGetWidth(_headerContainerView.bounds) / 2.0, CGRectGetHeight(_headerContainerView.bounds) / 2.0); } return _loadMoreActivityIndicatorView; } - (void)setLoadingMoreMessage:(BOOL)loadingMoreMessage { _loadingMoreMessage = loadingMoreMessage; if (loadingMoreMessage) { [self.loadMoreActivityIndicatorView startAnimating]; } else { [self.loadMoreActivityIndicatorView stopAnimating]; } } - (void)setLoadMoreActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)loadMoreActivityIndicatorViewStyle { _loadMoreActivityIndicatorViewStyle = loadMoreActivityIndicatorViewStyle; self.loadMoreActivityIndicatorView.activityIndicatorViewStyle = loadMoreActivityIndicatorViewStyle; } - (XHShareMenuView *)shareMenuView { if (!_shareMenuView) { XHShareMenuView *shareMenuView = [[XHShareMenuView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), self.keyboardViewHeight)]; shareMenuView.delegate = self; shareMenuView.backgroundColor = [UIColor colorWithWhite:0.961 alpha:1.000]; shareMenuView.alpha = 0.0; shareMenuView.shareMenuItems = self.shareMenuItems; [self.view addSubview:shareMenuView]; _shareMenuView = shareMenuView; } [self.view bringSubviewToFront:_shareMenuView]; return _shareMenuView; } - (XHEmotionManagerView *)emotionManagerView { if (!_emotionManagerView) { CGFloat hh = ((ScreenWidth-80)/4)*3+48; XHEmotionManagerView *emotionManagerView = [[XHEmotionManagerView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.bounds), ScreenWidth, Adapted(hh))]; emotionManagerView.delegate = self; emotionManagerView.dataSource = self; emotionManagerView.backgroundColor = RGB(250, 249, 249); emotionManagerView.alpha = 0.0; [self.view addSubview:emotionManagerView]; _emotionManagerView = emotionManagerView; } [self.view bringSubviewToFront:_emotionManagerView]; return _emotionManagerView; } - (XHVoiceRecordHUD *)voiceRecordHUD { if (!_voiceRecordHUD) { _voiceRecordHUD = [[XHVoiceRecordHUD alloc] initWithFrame:CGRectMake(0, 0, 140, 140)]; } return _voiceRecordHUD; } - (XHPhotographyHelper *)photographyHelper { if (!_photographyHelper) { _photographyHelper = [[XHPhotographyHelper alloc] init]; } return _photographyHelper; } - (XHLocationHelper *)locationHelper { if (!_locationHelper) { _locationHelper = [[XHLocationHelper alloc] init]; } return _locationHelper; } - (XHVoiceRecordHelper *)voiceRecordHelper { if (!_voiceRecordHelper) { _isMaxTimeStop = NO; WEAKSELF _voiceRecordHelper = [[XHVoiceRecordHelper alloc] init]; _voiceRecordHelper.maxTimeStopRecorderCompletion = ^{ DLog(@"已经达到最大限制时间了,进入下一步的提示"); // Unselect and unhilight the hold down button, and set isMaxTimeStop to YES. UIButton *holdDown = weakSelf.messageInputView.holdDownButton; holdDown.selected = NO; holdDown.highlighted = NO; weakSelf.isMaxTimeStop = YES; // add by lsz 2021-06-09 [[JSToastDialogs shareInstance] makeToast:@"语音最长15秒" duration:3.0]; //,文字最多15个 [weakSelf finishRecorded]; }; _voiceRecordHelper.peakPowerForChannel = ^(float peakPowerForChannel) { weakSelf.voiceRecordHUD.peakPower = peakPowerForChannel; }; _voiceRecordHelper.recordProgress = ^(float progress) { weakSelf.voiceRecordHUD.recordingTime = progress; }; _voiceRecordHelper.maxRecordTime = kVoiceRecorderTotalTime; } return _voiceRecordHelper; } #pragma mark - Messages View Controller - (void)finishSendMessageWithBubbleMessageType:(XHBubbleMessageMediaType)mediaType { switch (mediaType) { case XHBubbleMessageMediaTypeText: { [self.messageInputView.inputTextView setText:nil]; if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) { self.messageInputView.inputTextView.enablesReturnKeyAutomatically = NO; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.messageInputView.inputTextView.enablesReturnKeyAutomatically = YES; [self.messageInputView.inputTextView reloadInputViews]; }); } break; } case XHBubbleMessageMediaTypePhoto: { break; } case XHBubbleMessageMediaTypeVideo: { break; } case XHBubbleMessageMediaTypeVoice: { break; } case XHBubbleMessageMediaTypeEmotion: { break; } case XHBubbleMessageMediaTypeLocalPosition: { break; } default: break; } } - (void)setBackgroundColor:(UIColor *)color { self.view.backgroundColor = color; _messageTableView.backgroundColor = color; } - (void)setBackgroundImage:(UIImage *)backgroundImage { self.messageTableView.backgroundView = nil; self.messageTableView.backgroundView = [[UIImageView alloc] initWithImage:backgroundImage]; } - (void)scrollToBottomAnimated:(BOOL)animated { if (![self shouldAllowScroll]) return; NSInteger rows = [self.messageTableView numberOfRowsInSection:0]; if (rows > 0) { [self.messageTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:rows - 1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated]; } } - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)position animated:(BOOL)animated { if (![self shouldAllowScroll]) return; [self.messageTableView scrollToRowAtIndexPath:indexPath atScrollPosition:position animated:animated]; } #pragma mark - Previte Method - (BOOL)shouldAllowScroll { if (self.isUserScrolling) { if ([self.delegate respondsToSelector:@selector(shouldPreventScrollToBottomWhileUserScrolling)] && [self.delegate shouldPreventScrollToBottomWhileUserScrolling]) { return NO; } } return YES; } #pragma mark - Life Cycle - (void)setup { // iPhone or iPad keyboard view height set here. self.keyboardViewHeight = (kIsiPad ? 264 : 216); _allowsPanToDismissKeyboard = NO; _allowsSendText = YES; _allowsSendVoice = YES; _allowsSendMultiMedia = YES; _allowsSendFace = YES; _inputViewStyle = XHMessageInputViewStyleFlat; self.delegate = self; self.msgDataSource = self; } - (id)init { self = [super init]; if (self) { [self setup]; } return self; } - (void)awakeFromNib { [self setup]; } //xTT - (void)applicationWillResignActive:(NSNotification *)notification{ if (self.voiceRecordHelper.recorder.isRecording) { [self cancelRecord]; } [self.currentSelectedCell.messageBubbleView.animationVoiceImageView stopAnimating]; [[XHAudioPlayerHelper shareInstance] stopAudio]; } - (void)initilzer { if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)]) { self.automaticallyAdjustsScrollViewInsets = NO; } if ([self respondsToSelector:@selector(edgesForExtendedLayout)]) { self.edgesForExtendedLayout = UIRectEdgeNone; } //xTT UIApplication *app = [UIApplication sharedApplication]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app]; // 默认设置用户滚动为NO _isUserScrolling = NO; // 初始化message tableView XHMessageTableView *messageTableView = [[XHMessageTableView alloc] initWithFrame:CGRectMake(0, iPhoneX_NavHeight, ScreenWidth, ScreenHeight-iPhoneX_NavHeight) style:UITableViewStylePlain]; messageTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; messageTableView.dataSource = self; messageTableView.delegate = self; messageTableView.separatorColor = [UIColor clearColor]; messageTableView.separatorStyle = UITableViewCellSeparatorStyleNone; BOOL shouldLoadMoreMessagesScrollToTop = YES; if ([self.delegate respondsToSelector:@selector(shouldLoadMoreMessagesScrollToTop)]) { shouldLoadMoreMessagesScrollToTop = [self.delegate shouldLoadMoreMessagesScrollToTop]; } if (shouldLoadMoreMessagesScrollToTop) { messageTableView.tableHeaderView = self.headerContainerView; } [self.view addSubview:messageTableView]; [self.view sendSubviewToBack:messageTableView]; _messageTableView = messageTableView; // 设置Message TableView 的bottom edg // CGFloat inputViewHeight = (self.inputViewStyle == XHMessageInputViewStyleFlat) ? 45.0f : 40.0f; _defInputFrameHeight = TabBarHeight; // if([UIScreen mainScreen].bounds.size.height >= 812.0){ // _defInputFrameHeight += 35; // } [self setTableViewInsetsWithBottomValue:_defInputFrameHeight]; // 设置整体背景颜色 [self setBackgroundColor:[UIColor whiteColor]]; // 输入工具条的frame CGRect inputFrame = CGRectMake(0.0f, self.view.frame.size.height - _defInputFrameHeight, self.view.frame.size.width, _defInputFrameHeight); WEAKSELF if (self.allowsPanToDismissKeyboard) { // 控制输入工具条的位置块 void (^AnimationForMessageInputViewAtPoint)(CGPoint point) = ^(CGPoint point) { CGRect inputViewFrame = weakSelf.messageInputView.frame; CGPoint keyboardOrigin = [weakSelf.view convertPoint:point fromView:nil]; inputViewFrame.origin.y = keyboardOrigin.y - inputViewFrame.size.height; weakSelf.messageInputView.frame = inputViewFrame; }; self.messageTableView.keyboardDidScrollToPoint = ^(CGPoint point) { if (weakSelf.textViewInputViewType == XHInputViewTypeText) AnimationForMessageInputViewAtPoint(point); }; self.messageTableView.keyboardWillSnapBackToPoint = ^(CGPoint point) { if (weakSelf.textViewInputViewType == XHInputViewTypeText) AnimationForMessageInputViewAtPoint(point); }; self.messageTableView.keyboardWillBeDismissed = ^() { CGRect inputViewFrame = weakSelf.messageInputView.frame; inputViewFrame.origin.y = weakSelf.view.bounds.size.height - inputViewFrame.size.height; weakSelf.messageInputView.frame = inputViewFrame; }; } // block回调键盘通知 self.messageTableView.keyboardWillChange = ^(CGRect keyboardRect, UIViewAnimationOptions options, double duration, BOOL showKeyboard) { if (weakSelf.textViewInputViewType == XHInputViewTypeText) { [UIView animateWithDuration:duration delay:0.0 options:options animations:^{ CGFloat keyboardY = [weakSelf.view convertRect:keyboardRect fromView:nil].origin.y; CGFloat inputViewHeigght = showKeyboard ? 60 : TabBarHeight; weakSelf.messageInputView.size = CGSizeMake(weakSelf.messageInputView.frame.size.width, inputViewHeigght); CGRect inputViewFrame = weakSelf.messageInputView.frame; CGFloat inputViewFrameY = keyboardY - inputViewFrame.size.height; // for ipad modal form presentations CGFloat messageViewFrameBottom = weakSelf.view.frame.size.height - inputViewFrame.size.height; if (inputViewFrameY > messageViewFrameBottom) inputViewFrameY = messageViewFrameBottom; weakSelf.messageInputView.frame = CGRectMake(inputViewFrame.origin.x, inputViewFrameY, inputViewFrame.size.width, inputViewFrame.size.height); [weakSelf setTableViewInsetsWithBottomValue:weakSelf.view.frame.size.height - weakSelf.messageInputView.frame.origin.y]; if (showKeyboard) [weakSelf scrollToBottomAnimated:NO]; } completion:nil]; } }; self.messageTableView.keyboardDidChange = ^(BOOL didShowed) { if ([weakSelf.messageInputView.inputTextView isFirstResponder]) { if (didShowed) { if (weakSelf.textViewInputViewType == XHInputViewTypeText) { weakSelf.shareMenuView.alpha = 0.0; weakSelf.emotionManagerView.alpha = 0.0; } } } }; self.messageTableView.keyboardDidHide = ^() { [weakSelf.messageInputView.inputTextView resignFirstResponder]; }; // 初始化输入工具条 XHMessageInputView *inputView = [[XHMessageInputView alloc] initWithFrame:inputFrame]; inputView.allowsSendText = self.allowsSendText; inputView.allowsSendFace = self.allowsSendFace; inputView.allowsSendVoice = self.allowsSendVoice; inputView.allowsSendMultiMedia = self.allowsSendMultiMedia; inputView.delegate = self; [self.view addSubview:inputView]; [self.view bringSubviewToFront:inputView]; _messageInputView = inputView; // 默认发送语音 add by lsz 20201206 [_messageInputView messageStyleButtonClicked:_messageInputView.voiceChangeButton]; // _messageInputView.voiceChangeButton.enabled = NO; // _messageInputView.voiceChangeButton.hidden = YES; /// 隐藏+号按钮 // _messageInputView.multiMediaSendButton.enabled = self.chatType == 111 : YES : NO; // _messageInputView.multiMediaSendButton.hidden = self.chatType == 111 ? NO : YES; _messageInputView.allowsSendMultiMedia = self.chatType == 111 ? NO : YES; // 设置手势滑动,默认添加一个bar的高度值 self.messageTableView.messageInputBarHeight = CGRectGetHeight(_messageInputView.bounds); } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 设置键盘通知或者手势控制键盘消失 [self.messageTableView setupPanGestureControlKeyboardHide:self.allowsPanToDismissKeyboard]; // KVO 检查contentSize [self.messageInputView.inputTextView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil]; [self.messageInputView.inputTextView setEditable:YES]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if (self.textViewInputViewType != XHInputViewTypeNormal) { [self layoutOtherMenuViewHiden:YES]; } // remove键盘通知或者手势 [self.messageTableView disSetupPanGestureControlKeyboardHide:self.allowsPanToDismissKeyboard]; // remove KVO [self.messageInputView.inputTextView removeObserver:self forKeyPath:@"contentSize"]; [self.messageInputView.inputTextView setEditable:NO]; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. // 初始化消息页面布局 self.allowsSendMultiMedia = self.chatType == 111 ? YES : NO; [self initilzer]; [[XHMessageBubbleView appearance] setFont:[UIFont systemFontOfSize:16.0f]]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } - (void)dealloc { _messages = nil; _delegate = nil; _messageTableView.delegate = nil; _messageTableView.dataSource = nil; _messageTableView = nil; _messageInputView = nil; _photographyHelper = nil; _locationHelper = nil; } #pragma mark - View Rotation - (BOOL)shouldAutorotate { return NO; } #if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_9_0 - (NSUInteger)supportedInterfaceOrientations #else - (UIInterfaceOrientationMask)supportedInterfaceOrientations #endif { return UIInterfaceOrientationMaskPortrait; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { return UIInterfaceOrientationPortrait; } #pragma mark - RecorderPath Helper Method - (NSString *)getRecorderPath { NSString *recorderPath = nil; recorderPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex: 0]; NSDate *now = [NSDate date]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyyMMddHHmmssSSS"]; recorderPath = [recorderPath stringByAppendingPathComponent:[dateFormatter stringFromDate:now]]; recorderPath = [recorderPath stringByAppendingPathExtension:@"wav"]; return recorderPath; } #pragma mark - UITextView Helper Method - (CGFloat)getTextViewContentH:(UITextView *)textView { if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) { return ceilf([textView sizeThatFits:textView.frame.size].height); } else { return textView.contentSize.height; } } #pragma mark - Layout Message Input View Helper Method - (void)layoutAndAnimateMessageInputTextView:(UITextView *)textView { CGFloat maxHeight = [XHMessageInputView maxHeight]; CGFloat contentH = [self getTextViewContentH:textView]; BOOL isShrinking = contentH < self.previousTextViewContentHeight; CGFloat changeInHeight = contentH - _previousTextViewContentHeight; if (!isShrinking && (self.previousTextViewContentHeight == maxHeight || textView.text.length == 0)) { changeInHeight = 0; } else { changeInHeight = MIN(changeInHeight, maxHeight - self.previousTextViewContentHeight); } if (changeInHeight != 0.0f) { [UIView animateWithDuration:0.25f animations:^{ [self setTableViewInsetsWithBottomValue:self.messageTableView.contentInset.bottom + changeInHeight]; [self scrollToBottomAnimated:NO]; if (isShrinking) { if ([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0) { self.previousTextViewContentHeight = MIN(contentH, maxHeight); } // if shrinking the view, animate text view frame BEFORE input view frame [self.messageInputView adjustTextViewHeightBy:changeInHeight]; } CGRect inputViewFrame = self.messageInputView.frame; self.messageInputView.frame = CGRectMake(0.0f, inputViewFrame.origin.y - changeInHeight, inputViewFrame.size.width, inputViewFrame.size.height + changeInHeight); if (!isShrinking) { if ([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0) { self.previousTextViewContentHeight = MIN(contentH, maxHeight); } // growing the view, animate the text view frame AFTER input view frame [self.messageInputView adjustTextViewHeightBy:changeInHeight]; } } completion:^(BOOL finished) { }]; self.previousTextViewContentHeight = MIN(contentH, maxHeight); } // Once we reached the max height, we have to consider the bottom offset for the text view. // To make visible the last line, again we have to set the content offset. if (self.previousTextViewContentHeight == maxHeight) { double delayInSeconds = 0.01; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { CGPoint bottomOffset = CGPointMake(0.0f, contentH - textView.bounds.size.height); [textView setContentOffset:bottomOffset animated:YES]; }); } } #pragma mark - Scroll Message TableView Helper Method - (void)setTableViewInsetsWithBottomValue:(CGFloat)bottom { UIEdgeInsets insets = [self tableViewInsetsWithBottomValue:bottom]; self.messageTableView.contentInset = insets; self.messageTableView.scrollIndicatorInsets = insets; } - (UIEdgeInsets)tableViewInsetsWithBottomValue:(CGFloat)bottom { UIEdgeInsets insets = UIEdgeInsetsZero; if ([self respondsToSelector:@selector(topLayoutGuide)]) { // insets.top = self.topLayoutGuide.length; insets.top = 0; } insets.bottom = bottom; return insets; } #pragma mark - Message Calculate Cell Height - (CGFloat)calculateCellHeightWithMessage:(XHMessage *)message atIndexPath:(NSIndexPath *)indexPath { CGFloat cellHeight = 0; BOOL displayTimestamp = [message shouldShowTimestamp]; if ([self.delegate respondsToSelector:@selector(shouldDisplayTimestampForRowAtIndexPath:)]) { displayTimestamp = [self.delegate shouldDisplayTimestampForRowAtIndexPath:indexPath]; } cellHeight = [XHMessageTableViewCell calculateCellHeightWithMessage:message displaysTimestamp:displayTimestamp]; return cellHeight; } #pragma mark - Message Send helper Method - (void)didSendMessageWithText:(NSString *)text { DLog(@"send text : %@", text); if ([self.delegate respondsToSelector:@selector(didSendText:fromSender:onDate:)]) { [self.delegate didSendText:text fromSender:self.messageSender onDate:[NSDate date]]; } } - (void)didSendMessageWithPhoto:(UIImage *)photo { DLog(@"send photo : %@", photo); if ([self.delegate respondsToSelector:@selector(didSendPhoto:fromSender:onDate:)]) { [self.delegate didSendPhoto:photo fromSender:self.messageSender onDate:[NSDate date]]; } } - (void)didSendMessageWithVideoConverPhoto:(UIImage *)videoConverPhoto videoPath:(NSString *)videoPath { DLog(@"send videoPath : %@ videoConverPhoto : %@", videoPath, videoConverPhoto); if ([self.delegate respondsToSelector:@selector(didSendVideoConverPhoto:videoPath:fromSender:onDate:)]) { [self.delegate didSendVideoConverPhoto:videoConverPhoto videoPath:videoPath fromSender:self.messageSender onDate:[NSDate date]]; } } - (void)didSendMessageWithVoice:(NSString *)voicePath voiceDuration:(NSNumber *)voiceDuration { DLog(@"send voicePath : %@", voicePath); @try { if ([self.delegate respondsToSelector:@selector(didSendVoice:voiceDuration:fromSender:onDate:)]) { [self.delegate didSendVoice:voicePath voiceDuration:voiceDuration fromSender:self.messageSender onDate:[NSDate date]]; } } @catch (NSException *exception) { } @finally { } } - (void)didSendEmotionMessageWithEmotionPath:(NSString *)emotionPath { DLog(@"send emotionPath : %@", emotionPath); if ([self.delegate respondsToSelector:@selector(didSendEmotion:fromSender:onDate:)]) { [self.delegate didSendEmotion:emotionPath fromSender:self.messageSender onDate:[NSDate date]]; } } - (void)didSendGeolocationsMessageWithGeolocaltions:(NSString *)geolcations location:(CLLocation *)location { DLog(@"send geolcations : %@", geolcations); if ([self.delegate respondsToSelector:@selector(didSendGeoLocationsPhoto:geolocations:location:fromSender:onDate:)]) { [self.delegate didSendGeoLocationsPhoto:[UIImage imageNamed:@"Fav_Cell_Loc"] geolocations:geolcations location:location fromSender:self.messageSender onDate:[NSDate date]]; } } #pragma mark - Other Menu View Frame Helper Mehtod - (void)layoutOtherMenuViewHiden:(BOOL)hide { CGFloat inputViewHeigght = !hide ? 60 : TabBarHeight; self.messageInputView.size = CGSizeMake(self.messageInputView.frame.size.width, inputViewHeigght); [self.messageInputView.inputTextView resignFirstResponder]; [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ __block CGRect inputViewFrame = self.messageInputView.frame; __block CGRect otherMenuViewFrame; void (^InputViewAnimation)(BOOL hide) = ^(BOOL hide) { inputViewFrame.origin.y = (hide ? (CGRectGetHeight(self.view.bounds) - CGRectGetHeight(inputViewFrame)) : (CGRectGetMinY(otherMenuViewFrame) - CGRectGetHeight(inputViewFrame))); self.messageInputView.frame = inputViewFrame; }; void (^EmotionManagerViewAnimation)(BOOL hide) = ^(BOOL hide) { otherMenuViewFrame = self.emotionManagerView.frame; otherMenuViewFrame.origin.y = (hide ? CGRectGetHeight(self.view.frame) : (CGRectGetHeight(self.view.frame) - CGRectGetHeight(otherMenuViewFrame))); self.emotionManagerView.alpha = !hide; self.emotionManagerView.frame = otherMenuViewFrame; }; void (^ShareMenuViewAnimation)(BOOL hide) = ^(BOOL hide) { otherMenuViewFrame = self.shareMenuView.frame; otherMenuViewFrame.origin.y = (hide ? CGRectGetHeight(self.view.frame) : (CGRectGetHeight(self.view.frame) - CGRectGetHeight(otherMenuViewFrame))); self.shareMenuView.alpha = !hide; self.shareMenuView.frame = otherMenuViewFrame; }; if (hide) { switch (self.textViewInputViewType) { case XHInputViewTypeEmotion: { EmotionManagerViewAnimation(hide); break; } case XHInputViewTypeShareMenu: { ShareMenuViewAnimation(hide); break; } default: break; } } else { // 这里需要注意block的执行顺序,因为otherMenuViewFrame是公用的对象,所以对于被隐藏的Menu的frame的origin的y会是最大值 switch (self.textViewInputViewType) { case XHInputViewTypeEmotion: { // 1、先隐藏和自己无关的View ShareMenuViewAnimation(!hide); // 2、再显示和自己相关的View EmotionManagerViewAnimation(hide); break; } case XHInputViewTypeShareMenu: { // 1、先隐藏和自己无关的View EmotionManagerViewAnimation(!hide); // 2、再显示和自己相关的View ShareMenuViewAnimation(hide); break; } default: break; } } InputViewAnimation(hide); [self setTableViewInsetsWithBottomValue:self.view.frame.size.height - self.messageInputView.frame.origin.y]; [self scrollToBottomAnimated:NO]; } completion:^(BOOL finished) { if (hide) { self.textViewInputViewType = XHInputViewTypeNormal; } }]; } #pragma mark - Voice Recording Helper Method - (void)prepareRecordWithCompletion:(XHPrepareRecorderCompletion)completion { [self.voiceRecordHelper prepareRecordingWithPath:[self getRecorderPath] prepareRecorderCompletion:completion]; } - (void)startRecord { [self.voiceRecordHUD startRecordingHUDAtView:self.view]; [self.voiceRecordHelper startRecordingWithStartRecorderCompletion:^{ }]; } - (void)finishRecorded { WEAKSELF [self.voiceRecordHUD stopRecordCompled:^(BOOL fnished) { weakSelf.voiceRecordHUD = nil; }]; [self.voiceRecordHelper stopRecordingWithStopRecorderCompletion:^{ [weakSelf didSendMessageWithVoice:weakSelf.voiceRecordHelper.recordPath voiceDuration:weakSelf.voiceRecordHelper.recordDuration]; }]; } - (void)pauseRecord { [self.voiceRecordHUD pauseRecord]; } - (void)resumeRecord { [self.voiceRecordHUD resaueRecord]; } - (void)cancelRecord { WEAKSELF [self.voiceRecordHUD cancelRecordCompled:^(BOOL fnished) { weakSelf.voiceRecordHUD = nil; }]; [self.voiceRecordHelper cancelledDeleteWithCompletion:^{ }]; } #pragma mark - XHMessageInputView Delegate - (void)inputTextViewWillBeginEditing:(XHMessageTextView *)messageInputTextView { self.textViewInputViewType = XHInputViewTypeText; } - (void)inputTextViewDidBeginEditing:(XHMessageTextView *)messageInputTextView { if (!self.previousTextViewContentHeight) self.previousTextViewContentHeight = [self getTextViewContentH:messageInputTextView]; } - (void)didChangeSendVoiceAction:(BOOL)changed { if (changed) { if (self.textViewInputViewType == XHInputViewTypeText) return; // 在这之前,textViewInputViewType已经不是XHTextViewTextInputType [self layoutOtherMenuViewHiden:YES]; } } - (void)didSendTextAction:(NSString *)text { DLog(@"text : %@", text); if ([self.delegate respondsToSelector:@selector(didSendText:fromSender:onDate:)]) { [self.delegate didSendText:text fromSender:self.messageSender onDate:[NSDate date]]; } } - (void)didSelectedMultipleMediaAction { DLog(@"didSelectedMultipleMediaAction"); // self.textViewInputViewType = XHInputViewTypeShareMenu; self.textViewInputViewType = XHInputViewTypeNormal; [self layoutOtherMenuViewHiden:YES]; if ([self.delegate respondsToSelector:@selector(didSendVideoConverPhoto:videoPath:fromSender:onDate:)]) { [self.delegate didSendVideoConverPhoto:nil videoPath:nil fromSender:nil onDate:nil]; } } /// 表情/文字切换 - (void)didSendFaceAction:(BOOL)sendFace { if (sendFace) { self.textViewInputViewType = XHInputViewTypeEmotion; [self layoutOtherMenuViewHiden:NO]; } else { [self.messageInputView.inputTextView becomeFirstResponder]; } } - (void)prepareRecordingVoiceActionWithCompletion:(BOOL (^)(void))completion { DLog(@"prepareRecordingWithCompletion"); [self prepareRecordWithCompletion:completion]; } - (void)didStartRecordingVoiceAction { DLog(@"didStartRecordingVoice"); [self startRecord]; } - (void)didCancelRecordingVoiceAction { DLog(@"didCancelRecordingVoice"); [self cancelRecord]; } - (void)didFinishRecoingVoiceAction { DLog(@"didFinishRecoingVoice"); if (self.isMaxTimeStop == NO) { [self finishRecorded]; } else { self.isMaxTimeStop = NO; } } - (void)didDragOutsideAction { DLog(@"didDragOutsideAction"); [self resumeRecord]; } - (void)didDragInsideAction { DLog(@"didDragInsideAction"); [self pauseRecord]; } #pragma mark - XHShareMenuView Delegate - (void)didSelecteShareMenuItem:(XHShareMenuItem *)shareMenuItem atIndex:(NSInteger)index { DLog(@"title : %@ index:%ld", shareMenuItem.title, (long)index); WEAKSELF void (^PickerMediaBlock)(UIImage *image, NSDictionary *editingInfo) = ^(UIImage *image, NSDictionary *editingInfo) { if (image) { [weakSelf didSendMessageWithPhoto:image]; } else { if (!editingInfo) return ; NSString *mediaType = [editingInfo objectForKey: UIImagePickerControllerMediaType]; NSString *videoPath; NSURL *videoUrl; if (CFStringCompare ((__bridge CFStringRef) mediaType, kUTTypeMovie, 0) == kCFCompareEqualTo) { videoUrl = (NSURL*)[editingInfo objectForKey:UIImagePickerControllerMediaURL]; videoPath = [videoUrl path]; UIImage *thumbnailImage = [XHMessageVideoConverPhotoFactory videoConverPhotoWithVideoPath:videoPath]; [weakSelf didSendMessageWithVideoConverPhoto:thumbnailImage videoPath:videoPath]; } else { [weakSelf didSendMessageWithPhoto:[editingInfo valueForKey:UIImagePickerControllerOriginalImage]]; } } }; switch (index) { case 0: { // [self.photographyHelper showOnPickerViewControllerSourceType:UIImagePickerControllerSourceTypePhotoLibrary onViewController:self compled:PickerMediaBlock]; [weakSelf didSendMessageWithVideoConverPhoto:nil videoPath:nil]; break; } case 1: { [self.photographyHelper showOnPickerViewControllerSourceType:UIImagePickerControllerSourceTypeCamera onViewController:self compled:PickerMediaBlock]; break; } case 2: { WEAKSELF [self.locationHelper getCurrentGeolocationsCompled:^(NSArray *placemarks) { CLPlacemark *placemark = [placemarks lastObject]; if (placemark) { NSDictionary *addressDictionary = placemark.addressDictionary; NSArray *formattedAddressLines = [addressDictionary valueForKey:@"FormattedAddressLines"]; NSString *geoLocations = [formattedAddressLines lastObject]; if (geoLocations) { [weakSelf didSendGeolocationsMessageWithGeolocaltions:geoLocations location:placemark.location]; } } }]; break; } case 4:{ break; } default: break; } } #pragma mark - XHEmotionManagerView Delegate - (void)didSelecteEmotion:(XHEmotion *)emotion atIndexPath:(NSIndexPath *)indexPath { [self didSendEmotionMessageWithEmotionPath:emotion.emotionPath]; } #pragma mark - XHEmotionManagerView DataSource - (NSInteger)numberOfEmotionManagers { return 0; } - (XHEmotionManager *)emotionManagerForColumn:(NSInteger)column { return nil; } - (NSArray *)emotionManagersAtManager { return nil; } #pragma mark - UIScrollView Delegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if ([self.delegate respondsToSelector:@selector(shouldLoadMoreMessagesScrollToTop)]) { BOOL shouldLoadMoreMessages = [self.delegate shouldLoadMoreMessagesScrollToTop]; if (shouldLoadMoreMessages) { if (scrollView.contentOffset.y >= 0 && scrollView.contentOffset.y <= 44) { if (!self.loadingMoreMessage) { if ([self.delegate respondsToSelector:@selector(loadMoreMessagesScrollTotop)]) { [self.delegate loadMoreMessagesScrollTotop]; } } } } } } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { self.isUserScrolling = YES; UIMenuController *menu = [UIMenuController sharedMenuController]; if (menu.isMenuVisible) { [menu setMenuVisible:NO animated:YES]; } if (self.textViewInputViewType != XHInputViewTypeNormal) { [self layoutOtherMenuViewHiden:YES]; } } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { self.isUserScrolling = NO; } #pragma mark - XHMessageTableViewController Delegate - (BOOL)shouldPreventScrollToBottomWhileUserScrolling { return YES; } #pragma mark - XHMessageTableViewController DataSource - (XHMessage *)messageForRowAtIndexPath:(NSIndexPath *)indexPath { return [self getMessagesToShow][indexPath.row]; } #pragma mark - Table View Data Source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self getMessagesToShow].count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { XHMessage *message = [self.msgDataSource messageForRowAtIndexPath:indexPath]; // 如果需要定制复杂的业务UI,那么就实现该DataSource方法 if ([self.myDataSource respondsToSelector:@selector(tableView:cellForRowAtIndexPath:targetMessage:)]) { UITableViewCell *tableViewCell = [self.msgDataSource tableView:tableView cellForRowAtIndexPath:indexPath targetMessage:message]; return tableViewCell; } BOOL displayTimestamp = [message shouldShowTimestamp]; if ([self.delegate respondsToSelector:@selector(shouldDisplayTimestampForRowAtIndexPath:)]) { displayTimestamp = [self.delegate shouldDisplayTimestampForRowAtIndexPath:indexPath]; } if (displayTimestamp) { if (!_lastShowTimetamp || [_lastShowTimetamp doubleValue] < [message.timestamp doubleValue]) { _lastShowTimetamp = message.timestamp; } } static NSString *cellIdentifier = @"XHMessageTableViewCell"; XHMessageTableViewCell *messageTableViewCell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (!messageTableViewCell) { messageTableViewCell = [[XHMessageTableViewCell alloc] initWithMessage:message displaysTimestamp:displayTimestamp reuseIdentifier:cellIdentifier]; messageTableViewCell.delegate = self; } messageTableViewCell.indexPath = indexPath; [messageTableViewCell configureCellWithMessage:message displaysTimestamp:displayTimestamp]; //[messageTableViewCell setBackgroundColor:tableView.backgroundColor]; // UIImageView *line = [UIImageView new]; // line.backgroundColor = UIColor.blueColor; // [messageTableViewCell addSubview:line]; // [line mas_makeConstraints:^(MASConstraintMaker *make) { // make.left.right.equalTo(messageTableViewCell); // make.bottom.equalTo(messageTableViewCell.mas_bottom).inset(1); // make.height.mas_equalTo(1); // }]; if ([self.delegate respondsToSelector:@selector(configureCell:atIndexPath:)]) { [self.delegate configureCell:messageTableViewCell atIndexPath:indexPath]; } return messageTableViewCell; } #pragma mark - Table View Delegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { XHMessage *message = [self.msgDataSource messageForRowAtIndexPath:indexPath]; CGFloat calculateCellHeight = 0; if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:targetMessage:)]) { calculateCellHeight = [self.delegate tableView:tableView heightForRowAtIndexPath:indexPath targetMessage:message]; return calculateCellHeight; } else { calculateCellHeight = [self calculateCellHeightWithMessage:message atIndexPath:indexPath]; } return calculateCellHeight; } #pragma mark - Key-value Observing - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == self.messageInputView.inputTextView && [keyPath isEqualToString:@"contentSize"]) { [self layoutAndAnimateMessageInputTextView:object]; } } @end