You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1308 lines
47 KiB
1308 lines
47 KiB
![]()
2 years ago
|
//
|
||
|
// XHMessageTableViewController.m
|
||
|
// MessageDisplayExample
|
||
|
//
|
||
|
// Created by HUAJIE-1 on 14-4-24.
|
||
|
// Copyright (c) 2014年 嗨,我是曾宪华(@xhzengAIB),曾加入YY Inc.担任高级移动开发工程师,拍立秀App联合创始人,热衷于简洁、而富有理性的事物 QQ:543413507 主页:http://zengxianhua.com All rights reserved.
|
||
|
//
|
||
|
|
||
|
#import "XHMessageTableViewController.h"
|
||
|
#import <MobileCoreServices/MobileCoreServices.h>
|
||
|
#import "XHAudioPlayerHelper.h"
|
||
|
|
||
|
@interface XHMessageTableViewController ()
|
||
|
|
||
|
/**
|
||
|
* 判断是否用户手指滚动
|
||
|
*/
|
||
|
@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) XHPhotographyHelper *photographyHelper;
|
||
|
|
||
|
/**
|
||
|
* 管理地理位置的工具对象
|
||
|
*/
|
||
|
@property (nonatomic, strong) XHLocationHelper *locationHelper;
|
||
|
|
||
|
/**
|
||
|
* 管理录音工具对象
|
||
|
*/
|
||
|
@property (nonatomic, strong) XHVoiceRecordHelper *voiceRecordHelper;
|
||
|
|
||
|
/**
|
||
|
* 判断是不是超出了录音最大时长
|
||
|
*/
|
||
|
@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:(id <XHMessageModel>)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:(NSString*)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.messages];
|
||
|
[messages addObject:addedMessage];
|
||
|
|
||
|
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:1];
|
||
|
[indexPaths addObject:[NSIndexPath indexPathForRow:messages.count - 1 inSection:0]];
|
||
|
|
||
|
[weakSelf exMainQueue:^{
|
||
|
weakSelf.messages = messages;
|
||
|
[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:^(id 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 *)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), 44)];
|
||
|
_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) {
|
||
|
XHEmotionManagerView *emotionManagerView = [[XHEmotionManagerView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), self.keyboardViewHeight)];
|
||
|
emotionManagerView.delegate = self;
|
||
|
emotionManagerView.dataSource = self;
|
||
|
emotionManagerView.backgroundColor = [UIColor colorWithWhite:0.961 alpha:1.000];
|
||
|
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;
|
||
|
|
||
|
[[JSToastDialogs shareInstance] makeToast:GJText(@"语音最长15秒") duration:3.0];
|
||
|
|
||
|
[weakSelf finishRecorded];
|
||
|
};
|
||
|
_voiceRecordHelper.peakPowerForChannel = ^(float peakPowerForChannel) {
|
||
|
weakSelf.voiceRecordHUD.peakPower = peakPowerForChannel;
|
||
|
};
|
||
|
_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;
|
||
|
_allowsSendVoice = YES;
|
||
|
_allowsSendMultiMedia = YES;
|
||
|
_allowsSendFace = YES;
|
||
|
_inputViewStyle = XHMessageInputViewStyleFlat;
|
||
|
|
||
|
self.delegate = self;
|
||
|
self.dataSource = 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:self.view.bounds 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) ? iPhoneX_TabbarHeight : 40.0f;
|
||
|
[self setTableViewInsetsWithBottomValue:inputViewHeight];
|
||
|
|
||
|
// 设置整体背景颜色
|
||
|
[self setBackgroundColor:[UIColor whiteColor]];
|
||
|
|
||
|
// 输入工具条的frame
|
||
|
CGRect inputFrame = CGRectMake(0.0f,
|
||
|
self.view.frame.size.height - inputViewHeight,
|
||
|
self.view.frame.size.width,
|
||
|
inputViewHeight);
|
||
|
|
||
|
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;
|
||
|
|
||
|
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.allowsSendFace = self.allowsSendFace;
|
||
|
inputView.allowsSendVoice = self.allowsSendVoice;
|
||
|
inputView.allowsSendMultiMedia = self.allowsSendMultiMedia;
|
||
|
inputView.delegate = self;
|
||
|
[self.view addSubview:inputView];
|
||
|
[self.view bringSubviewToFront:inputView];
|
||
|
|
||
|
_messageInputView = inputView;
|
||
|
|
||
|
|
||
|
// 设置手势滑动,默认添加一个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 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;
|
||
|
_dataSource = 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(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex: 0];
|
||
|
NSDate *now = [NSDate date];
|
||
|
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||
|
[dateFormatter setDateFormat:@"yyyyMMddHHmmssSSS"];
|
||
|
//recorderPath = [recorderPath stringByAppendingFormat:@"%@-MySound.m4a", [dateFormatter stringFromDate:now]];
|
||
|
|
||
|
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.bottom = bottom;
|
||
|
|
||
|
return insets;
|
||
|
}
|
||
|
|
||
|
#pragma mark - Message Calculate Cell Height
|
||
|
|
||
|
- (CGFloat)calculateCellHeightWithMessage:(id <XHMessageModel>)message atIndexPath:(NSIndexPath *)indexPath {
|
||
|
CGFloat cellHeight = 0;
|
||
|
|
||
|
BOOL displayTimestamp = YES;
|
||
|
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:(NSString*)voiceDuration {
|
||
|
DLog(@"send voicePath : %@", voicePath);
|
||
|
if ([self.delegate respondsToSelector:@selector(didSendVoice:voiceDuration:fromSender:onDate:)]) {
|
||
|
[self.delegate didSendVoice:voicePath voiceDuration:voiceDuration fromSender:self.messageSender onDate:[NSDate date]];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- (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 {
|
||
|
[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 layoutOtherMenuViewHiden:NO];
|
||
|
}
|
||
|
|
||
|
- (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];
|
||
|
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
|
||
|
|
||
|
- (id <XHMessageModel>)messageForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||
|
return self.messages[indexPath.row];
|
||
|
}
|
||
|
|
||
|
#pragma mark - Table View Data Source
|
||
|
|
||
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||
|
return self.messages.count;
|
||
|
}
|
||
|
|
||
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||
|
|
||
|
id <XHMessageModel> message = [self.dataSource messageForRowAtIndexPath:indexPath];
|
||
|
|
||
|
// 如果需要定制复杂的业务UI,那么就实现该DataSource方法
|
||
|
if ([self.dataSource respondsToSelector:@selector(tableView:cellForRowAtIndexPath:targetMessage:)]) {
|
||
|
UITableViewCell *tableViewCell = [self.dataSource tableView:tableView cellForRowAtIndexPath:indexPath targetMessage:message];
|
||
|
return tableViewCell;
|
||
|
}
|
||
|
|
||
|
BOOL displayTimestamp = YES;
|
||
|
if ([self.delegate respondsToSelector:@selector(shouldDisplayTimestampForRowAtIndexPath:)]) {
|
||
|
displayTimestamp = [self.delegate shouldDisplayTimestampForRowAtIndexPath:indexPath];
|
||
|
}
|
||
|
|
||
|
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];
|
||
|
|
||
|
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 {
|
||
|
id <XHMessageModel> message = [self.dataSource 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
|