1
This commit is contained in:
@@ -43,29 +43,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cell 布局:
|
// Cell 布局:
|
||||||
// 左边距(68) + 头像(28) + 间距(8) + 内容区域 + 间距(8) + 点赞按钮(40) + 右边距(16)
|
// 左边距(68) + 头像(28) + 间距(8) + 内容区域 + 右边距(50)
|
||||||
// 内容区域宽度 = maxWidth - 68 - 28 - 8 - 8 - 40 - 16
|
// 内容区域宽度 = maxWidth - 68 - 28 - 8 - 50
|
||||||
CGFloat contentWidth = maxWidth - 68 - 28 - 8 - 8 - 40 - 16;
|
CGFloat contentWidth = maxWidth - 68 - 28 - 8 - 50;
|
||||||
|
|
||||||
// 构建富文本计算高度
|
// 用户名高度(可能包含 "回复 @xxx")
|
||||||
NSMutableString *fullText = [NSMutableString stringWithString:self.userName];
|
NSMutableString *userNameText = [NSMutableString stringWithString:self.userName];
|
||||||
if (self.replyToUserName.length > 0) {
|
if (self.replyToUserName.length > 0) {
|
||||||
[fullText appendFormat:@" 回复 @%@", self.replyToUserName];
|
[userNameText appendFormat:@" 回复 @%@", self.replyToUserName];
|
||||||
}
|
}
|
||||||
[fullText appendFormat:@":%@", self.content];
|
UIFont *userNameFont = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
|
||||||
|
CGRect userNameRect = [userNameText boundingRectWithSize:CGSizeMake(contentWidth, CGFLOAT_MAX)
|
||||||
|
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||||
|
attributes:@{NSFontAttributeName: userNameFont}
|
||||||
|
context:nil];
|
||||||
|
CGFloat userNameHeight = ceil(userNameRect.size.height);
|
||||||
|
|
||||||
|
// 内容高度
|
||||||
UIFont *contentFont = [UIFont systemFontOfSize:14];
|
UIFont *contentFont = [UIFont systemFontOfSize:14];
|
||||||
CGRect contentRect = [fullText boundingRectWithSize:CGSizeMake(contentWidth, CGFLOAT_MAX)
|
CGRect contentRect = [self.content boundingRectWithSize:CGSizeMake(contentWidth, CGFLOAT_MAX)
|
||||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||||
attributes:@{NSFontAttributeName: contentFont}
|
attributes:@{NSFontAttributeName: contentFont}
|
||||||
context:nil];
|
context:nil];
|
||||||
CGFloat contentHeight = ceil(contentRect.size.height);
|
CGFloat contentHeight = ceil(contentRect.size.height);
|
||||||
|
|
||||||
// 时间高度(单行)
|
// 时间高度(单行)
|
||||||
CGFloat timeHeight = 14; // 11号字体
|
CGFloat timeHeight = 14; // 11号字体
|
||||||
|
|
||||||
// 总高度 = 上边距(8) + 内容 + 间距(4) + 时间 + 下边距(8)
|
// 总高度 = 上边距(8) + 用户名 + 间距(4) + 内容 + 间距(6) + 时间 + 下边距(8)
|
||||||
CGFloat totalHeight = 8 + contentHeight + 4 + timeHeight + 8;
|
CGFloat totalHeight = 8 + userNameHeight + 4 + contentHeight + 6 + timeHeight + 8;
|
||||||
|
|
||||||
// 最小高度(头像高度 + 上下边距)
|
// 最小高度(头像高度 + 上下边距)
|
||||||
CGFloat minHeight = 8 + 28 + 8;
|
CGFloat minHeight = 8 + 28 + 8;
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 点赞按钮点击回调
|
/// 点赞按钮点击回调
|
||||||
@property(nonatomic, copy, nullable) void (^onLikeAction)(void);
|
@property(nonatomic, copy, nullable) void (^onLikeAction)(void);
|
||||||
|
|
||||||
|
/// 回复按钮点击回调
|
||||||
|
@property(nonatomic, copy, nullable) void (^onReplyAction)(void);
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#import "KBAICommentHeaderView.h"
|
#import "KBAICommentHeaderView.h"
|
||||||
#import "KBAICommentModel.h"
|
#import "KBAICommentModel.h"
|
||||||
|
#import "KBTopImageButton.h"
|
||||||
#import <Masonry/Masonry.h>
|
#import <Masonry/Masonry.h>
|
||||||
#import <SDWebImage/SDWebImage.h>
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
|
||||||
@@ -14,9 +15,10 @@
|
|||||||
|
|
||||||
@property(nonatomic, strong) UIImageView *avatarImageView;
|
@property(nonatomic, strong) UIImageView *avatarImageView;
|
||||||
@property(nonatomic, strong) UILabel *userNameLabel;
|
@property(nonatomic, strong) UILabel *userNameLabel;
|
||||||
@property(nonatomic, strong) UILabel *timeLabel;
|
|
||||||
@property(nonatomic, strong) UILabel *contentLabel;
|
@property(nonatomic, strong) UILabel *contentLabel;
|
||||||
@property(nonatomic, strong) UIButton *likeButton;
|
@property(nonatomic, strong) UILabel *timeLabel;
|
||||||
|
@property(nonatomic, strong) UIButton *replyButton;
|
||||||
|
@property(nonatomic, strong) KBTopImageButton *likeButton;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -37,8 +39,9 @@
|
|||||||
|
|
||||||
[self.contentView addSubview:self.avatarImageView];
|
[self.contentView addSubview:self.avatarImageView];
|
||||||
[self.contentView addSubview:self.userNameLabel];
|
[self.contentView addSubview:self.userNameLabel];
|
||||||
[self.contentView addSubview:self.timeLabel];
|
|
||||||
[self.contentView addSubview:self.contentLabel];
|
[self.contentView addSubview:self.contentLabel];
|
||||||
|
[self.contentView addSubview:self.timeLabel];
|
||||||
|
[self.contentView addSubview:self.replyButton];
|
||||||
[self.contentView addSubview:self.likeButton];
|
[self.contentView addSubview:self.likeButton];
|
||||||
|
|
||||||
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
@@ -53,24 +56,31 @@
|
|||||||
make.right.lessThanOrEqualTo(self.likeButton.mas_left).offset(-10);
|
make.right.lessThanOrEqualTo(self.likeButton.mas_left).offset(-10);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.userNameLabel);
|
make.left.equalTo(self.userNameLabel);
|
||||||
make.top.equalTo(self.userNameLabel.mas_bottom).offset(2);
|
make.top.equalTo(self.contentLabel.mas_bottom).offset(8);
|
||||||
|
make.bottom.equalTo(self.contentView).offset(-12).priority(MASLayoutPriorityDefaultHigh);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.replyButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.userNameLabel);
|
make.left.equalTo(self.timeLabel.mas_right).offset(16);
|
||||||
make.top.equalTo(self.timeLabel.mas_bottom).offset(8);
|
make.centerY.equalTo(self.timeLabel);
|
||||||
make.right.equalTo(self.contentView).offset(-50);
|
|
||||||
make.bottom.equalTo(self.contentView).offset(-12).priority(MASLayoutPriorityDefaultHigh);
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.right.equalTo(self.contentView).offset(-16);
|
make.right.equalTo(self.contentView).offset(-16);
|
||||||
make.top.equalTo(self.contentView).offset(12);
|
make.top.equalTo(self.contentView).offset(12);
|
||||||
make.width.mas_equalTo(50);
|
make.width.mas_equalTo(30);
|
||||||
make.height.mas_equalTo(40);
|
make.height.mas_equalTo(40);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.userNameLabel);
|
||||||
|
make.top.equalTo(self.userNameLabel.mas_bottom).offset(6);
|
||||||
|
make.right.equalTo(self.likeButton.mas_left).offset(-5);
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Configuration
|
#pragma mark - Configuration
|
||||||
@@ -80,19 +90,19 @@
|
|||||||
sd_setImageWithURL:[NSURL URLWithString:comment.avatarUrl]
|
sd_setImageWithURL:[NSURL URLWithString:comment.avatarUrl]
|
||||||
placeholderImage:[UIImage imageNamed:@"default_avatar"]];
|
placeholderImage:[UIImage imageNamed:@"default_avatar"]];
|
||||||
self.userNameLabel.text = comment.userName;
|
self.userNameLabel.text = comment.userName;
|
||||||
self.timeLabel.text = [comment formattedTime];
|
|
||||||
self.contentLabel.text = comment.content;
|
self.contentLabel.text = comment.content;
|
||||||
|
self.timeLabel.text = [comment formattedTime];
|
||||||
|
|
||||||
// 点赞按钮
|
// 点赞按钮
|
||||||
NSString *likeText =
|
NSString *likeText =
|
||||||
comment.likeCount > 0 ? [self formatLikeCount:comment.likeCount] : @"赞";
|
comment.likeCount > 0 ? [self formatLikeCount:comment.likeCount] : @"赞";
|
||||||
[self.likeButton setTitle:likeText forState:UIControlStateNormal];
|
self.likeButton.textLabel.text = likeText;
|
||||||
|
|
||||||
UIImage *likeImage = comment.isLiked
|
UIImage *likeImage = comment.isLiked
|
||||||
? [UIImage systemImageNamed:@"heart.fill"]
|
? [UIImage systemImageNamed:@"heart.fill"]
|
||||||
: [UIImage systemImageNamed:@"heart"];
|
: [UIImage systemImageNamed:@"heart"];
|
||||||
[self.likeButton setImage:likeImage forState:UIControlStateNormal];
|
self.likeButton.iconView.image = likeImage;
|
||||||
self.likeButton.tintColor =
|
self.likeButton.iconView.tintColor =
|
||||||
comment.isLiked ? [UIColor systemRedColor] : [UIColor grayColor];
|
comment.isLiked ? [UIColor systemRedColor] : [UIColor grayColor];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +123,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)replyButtonTapped {
|
||||||
|
if (self.onReplyAction) {
|
||||||
|
self.onReplyAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Lazy Loading
|
#pragma mark - Lazy Loading
|
||||||
|
|
||||||
- (UIImageView *)avatarImageView {
|
- (UIImageView *)avatarImageView {
|
||||||
@@ -129,22 +145,12 @@
|
|||||||
- (UILabel *)userNameLabel {
|
- (UILabel *)userNameLabel {
|
||||||
if (!_userNameLabel) {
|
if (!_userNameLabel) {
|
||||||
_userNameLabel = [[UILabel alloc] init];
|
_userNameLabel = [[UILabel alloc] init];
|
||||||
_userNameLabel.font = [UIFont systemFontOfSize:14
|
_userNameLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||||
weight:UIFontWeightMedium];
|
_userNameLabel.textColor = [UIColor secondaryLabelColor];
|
||||||
_userNameLabel.textColor = [UIColor labelColor];
|
|
||||||
}
|
}
|
||||||
return _userNameLabel;
|
return _userNameLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (UILabel *)timeLabel {
|
|
||||||
if (!_timeLabel) {
|
|
||||||
_timeLabel = [[UILabel alloc] init];
|
|
||||||
_timeLabel.font = [UIFont systemFontOfSize:12];
|
|
||||||
_timeLabel.textColor = [UIColor secondaryLabelColor];
|
|
||||||
}
|
|
||||||
return _timeLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UILabel *)contentLabel {
|
- (UILabel *)contentLabel {
|
||||||
if (!_contentLabel) {
|
if (!_contentLabel) {
|
||||||
_contentLabel = [[UILabel alloc] init];
|
_contentLabel = [[UILabel alloc] init];
|
||||||
@@ -155,19 +161,37 @@
|
|||||||
return _contentLabel;
|
return _contentLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (UIButton *)likeButton {
|
- (UILabel *)timeLabel {
|
||||||
if (!_likeButton) {
|
if (!_timeLabel) {
|
||||||
_likeButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
_timeLabel = [[UILabel alloc] init];
|
||||||
_likeButton.titleLabel.font = [UIFont systemFontOfSize:12];
|
_timeLabel.font = [UIFont systemFontOfSize:12];
|
||||||
[_likeButton setTitleColor:[UIColor grayColor]
|
_timeLabel.textColor = [UIColor secondaryLabelColor];
|
||||||
forState:UIControlStateNormal];
|
}
|
||||||
[_likeButton setImage:[UIImage systemImageNamed:@"heart"]
|
return _timeLabel;
|
||||||
forState:UIControlStateNormal];
|
}
|
||||||
_likeButton.tintColor = [UIColor grayColor];
|
|
||||||
|
|
||||||
// 图片在上,文字在下的布局
|
- (UIButton *)replyButton {
|
||||||
_likeButton.contentHorizontalAlignment =
|
if (!_replyButton) {
|
||||||
UIControlContentHorizontalAlignmentCenter;
|
_replyButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
|
_replyButton.titleLabel.font = [UIFont systemFontOfSize:12];
|
||||||
|
[_replyButton setTitle:@"回复" forState:UIControlStateNormal];
|
||||||
|
[_replyButton setTitleColor:[UIColor secondaryLabelColor] forState:UIControlStateNormal];
|
||||||
|
[_replyButton addTarget:self
|
||||||
|
action:@selector(replyButtonTapped)
|
||||||
|
forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
|
return _replyButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBTopImageButton *)likeButton {
|
||||||
|
if (!_likeButton) {
|
||||||
|
_likeButton = [[KBTopImageButton alloc] init];
|
||||||
|
_likeButton.iconSize = CGSizeMake(20, 20);
|
||||||
|
_likeButton.spacing = 2;
|
||||||
|
_likeButton.iconView.image = [UIImage systemImageNamed:@"heart"];
|
||||||
|
_likeButton.iconView.tintColor = [UIColor grayColor];
|
||||||
|
_likeButton.textLabel.font = [UIFont systemFontOfSize:11];
|
||||||
|
_likeButton.textLabel.textColor = [UIColor grayColor];
|
||||||
[_likeButton addTarget:self
|
[_likeButton addTarget:self
|
||||||
action:@selector(likeButtonTapped)
|
action:@selector(likeButtonTapped)
|
||||||
forControlEvents:UIControlEventTouchUpInside];
|
forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
|||||||
/// 输入框底部约束
|
/// 输入框底部约束
|
||||||
@property(nonatomic, strong) MASConstraint *inputBottomConstraint;
|
@property(nonatomic, strong) MASConstraint *inputBottomConstraint;
|
||||||
|
|
||||||
|
/// 当前回复的目标(一级评论)
|
||||||
|
@property(nonatomic, weak) KBAICommentModel *replyToComment;
|
||||||
|
/// 当前回复的目标(二级评论)
|
||||||
|
@property(nonatomic, weak) KBAIReplyModel *replyToReply;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBAICommentView
|
@implementation KBAICommentView
|
||||||
@@ -246,6 +251,10 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
|||||||
withRowAnimation:UITableViewRowAnimationNone];
|
withRowAnimation:UITableViewRowAnimationNone];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cell.onReplyAction = ^{
|
||||||
|
[weakSelf setReplyToComment:comment reply:reply];
|
||||||
|
};
|
||||||
|
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +277,10 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
|||||||
withRowAnimation:UITableViewRowAnimationNone];
|
withRowAnimation:UITableViewRowAnimationNone];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
header.onReplyAction = ^{
|
||||||
|
[weakSelf setReplyToComment:comment reply:nil];
|
||||||
|
};
|
||||||
|
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,12 +494,55 @@ static NSInteger const kRepliesLoadCount = 5;
|
|||||||
return _inputView;
|
return _inputView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Reply
|
||||||
|
|
||||||
|
- (void)setReplyToComment:(KBAICommentModel *)comment reply:(KBAIReplyModel *)reply {
|
||||||
|
self.replyToComment = comment;
|
||||||
|
self.replyToReply = reply;
|
||||||
|
|
||||||
|
if (reply) {
|
||||||
|
// 回复二级评论
|
||||||
|
self.inputView.placeholder = [NSString stringWithFormat:@"回复 @%@", reply.userName];
|
||||||
|
} else if (comment) {
|
||||||
|
// 回复一级评论
|
||||||
|
self.inputView.placeholder = [NSString stringWithFormat:@"回复 @%@", comment.userName];
|
||||||
|
} else {
|
||||||
|
// 普通评论
|
||||||
|
self.inputView.placeholder = @"说点什么...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)clearReplyTarget {
|
||||||
|
self.replyToComment = nil;
|
||||||
|
self.replyToReply = nil;
|
||||||
|
self.inputView.placeholder = @"说点什么...";
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Send Comment
|
#pragma mark - Send Comment
|
||||||
|
|
||||||
- (void)sendCommentWithText:(NSString *)text {
|
- (void)sendCommentWithText:(NSString *)text {
|
||||||
if (text.length == 0) return;
|
if (text.length == 0) return;
|
||||||
|
|
||||||
// 创建新评论
|
CGFloat tableWidth = self.tableView.bounds.size.width;
|
||||||
|
if (tableWidth <= 0) {
|
||||||
|
tableWidth = [UIScreen mainScreen].bounds.size.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.replyToComment) {
|
||||||
|
// 回复评论(添加二级评论)
|
||||||
|
[self sendReplyWithText:text tableWidth:tableWidth];
|
||||||
|
} else {
|
||||||
|
// 发送一级评论
|
||||||
|
[self sendNewCommentWithText:text tableWidth:tableWidth];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空输入框和回复目标
|
||||||
|
[self.inputView clearText];
|
||||||
|
[self clearReplyTarget];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)sendNewCommentWithText:(NSString *)text tableWidth:(CGFloat)tableWidth {
|
||||||
|
// 创建新一级评论
|
||||||
KBAICommentModel *newComment = [[KBAICommentModel alloc] init];
|
KBAICommentModel *newComment = [[KBAICommentModel alloc] init];
|
||||||
newComment.commentId = [NSUUID UUID].UUIDString;
|
newComment.commentId = [NSUUID UUID].UUIDString;
|
||||||
newComment.userId = @"current_user";
|
newComment.userId = @"current_user";
|
||||||
@@ -499,10 +555,6 @@ static NSInteger const kRepliesLoadCount = 5;
|
|||||||
newComment.replies = @[];
|
newComment.replies = @[];
|
||||||
|
|
||||||
// 计算高度缓存
|
// 计算高度缓存
|
||||||
CGFloat tableWidth = self.tableView.bounds.size.width;
|
|
||||||
if (tableWidth <= 0) {
|
|
||||||
tableWidth = [UIScreen mainScreen].bounds.size.width;
|
|
||||||
}
|
|
||||||
newComment.cachedHeaderHeight = [newComment calculateHeaderHeightWithMaxWidth:tableWidth];
|
newComment.cachedHeaderHeight = [newComment calculateHeaderHeightWithMaxWidth:tableWidth];
|
||||||
|
|
||||||
// 插入到数组第一个
|
// 插入到数组第一个
|
||||||
@@ -516,9 +568,69 @@ static NSInteger const kRepliesLoadCount = 5;
|
|||||||
|
|
||||||
// 滚动到顶部
|
// 滚动到顶部
|
||||||
[self.tableView setContentOffset:CGPointZero animated:YES];
|
[self.tableView setContentOffset:CGPointZero animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
// 清空输入框
|
- (void)sendReplyWithText:(NSString *)text tableWidth:(CGFloat)tableWidth {
|
||||||
[self.inputView clearText];
|
KBAICommentModel *comment = self.replyToComment;
|
||||||
|
if (!comment) return;
|
||||||
|
|
||||||
|
// 创建新二级评论
|
||||||
|
KBAIReplyModel *newReply = [[KBAIReplyModel alloc] init];
|
||||||
|
newReply.replyId = [NSUUID UUID].UUIDString;
|
||||||
|
newReply.userId = @"current_user";
|
||||||
|
newReply.userName = @"我";
|
||||||
|
newReply.avatarUrl = @"";
|
||||||
|
newReply.content = text;
|
||||||
|
newReply.likeCount = 0;
|
||||||
|
newReply.isLiked = NO;
|
||||||
|
newReply.createTime = [[NSDate date] timeIntervalSince1970];
|
||||||
|
|
||||||
|
// 如果是回复二级评论,设置被回复用户
|
||||||
|
if (self.replyToReply) {
|
||||||
|
newReply.replyToUserName = self.replyToReply.userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算高度缓存
|
||||||
|
newReply.cachedCellHeight = [newReply calculateCellHeightWithMaxWidth:tableWidth];
|
||||||
|
|
||||||
|
// 添加到 replies 数组
|
||||||
|
NSMutableArray *newReplies = [NSMutableArray arrayWithArray:comment.replies];
|
||||||
|
[newReplies addObject:newReply];
|
||||||
|
comment.replies = newReplies;
|
||||||
|
comment.totalReplyCount = newReplies.count;
|
||||||
|
|
||||||
|
// 找到该评论的 section
|
||||||
|
NSInteger section = [self.comments indexOfObject:comment];
|
||||||
|
if (section == NSNotFound) return;
|
||||||
|
|
||||||
|
// 如果已展开,添加到 displayedReplies 并插入行
|
||||||
|
if (comment.isRepliesExpanded) {
|
||||||
|
NSInteger newRowIndex = comment.displayedReplies.count;
|
||||||
|
[comment.displayedReplies addObject:newReply];
|
||||||
|
|
||||||
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:newRowIndex inSection:section];
|
||||||
|
[self.tableView insertRowsAtIndexPaths:@[indexPath]
|
||||||
|
withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||||
|
|
||||||
|
// 刷新 Footer
|
||||||
|
KBAICommentFooterView *footerView =
|
||||||
|
(KBAICommentFooterView *)[self.tableView footerViewForSection:section];
|
||||||
|
if (footerView) {
|
||||||
|
[footerView configureWithComment:comment];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到新回复
|
||||||
|
[self.tableView scrollToRowAtIndexPath:indexPath
|
||||||
|
atScrollPosition:UITableViewScrollPositionBottom
|
||||||
|
animated:YES];
|
||||||
|
} else {
|
||||||
|
// 未展开,刷新 Footer 显示新的回复数
|
||||||
|
KBAICommentFooterView *footerView =
|
||||||
|
(KBAICommentFooterView *)[self.tableView footerViewForSection:section];
|
||||||
|
if (footerView) {
|
||||||
|
[footerView configureWithComment:comment];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 点赞按钮点击回调
|
/// 点赞按钮点击回调
|
||||||
@property(nonatomic, copy, nullable) void (^onLikeAction)(void);
|
@property(nonatomic, copy, nullable) void (^onLikeAction)(void);
|
||||||
|
|
||||||
|
/// 回复按钮点击回调
|
||||||
|
@property(nonatomic, copy, nullable) void (^onReplyAction)(void);
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -7,15 +7,18 @@
|
|||||||
|
|
||||||
#import "KBAIReplyCell.h"
|
#import "KBAIReplyCell.h"
|
||||||
#import "KBAIReplyModel.h"
|
#import "KBAIReplyModel.h"
|
||||||
|
#import "KBTopImageButton.h"
|
||||||
#import <Masonry/Masonry.h>
|
#import <Masonry/Masonry.h>
|
||||||
#import <SDWebImage/SDWebImage.h>
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
|
||||||
@interface KBAIReplyCell ()
|
@interface KBAIReplyCell ()
|
||||||
|
|
||||||
@property(nonatomic, strong) UIImageView *avatarImageView;
|
@property(nonatomic, strong) UIImageView *avatarImageView;
|
||||||
|
@property(nonatomic, strong) UILabel *userNameLabel;
|
||||||
@property(nonatomic, strong) UILabel *contentLabel;
|
@property(nonatomic, strong) UILabel *contentLabel;
|
||||||
@property(nonatomic, strong) UILabel *timeLabel;
|
@property(nonatomic, strong) UILabel *timeLabel;
|
||||||
@property(nonatomic, strong) UIButton *likeButton;
|
@property(nonatomic, strong) UIButton *replyButton;
|
||||||
|
@property(nonatomic, strong) KBTopImageButton *likeButton;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -36,36 +39,48 @@
|
|||||||
|
|
||||||
- (void)setupUI {
|
- (void)setupUI {
|
||||||
[self.contentView addSubview:self.avatarImageView];
|
[self.contentView addSubview:self.avatarImageView];
|
||||||
|
[self.contentView addSubview:self.userNameLabel];
|
||||||
[self.contentView addSubview:self.contentLabel];
|
[self.contentView addSubview:self.contentLabel];
|
||||||
[self.contentView addSubview:self.timeLabel];
|
[self.contentView addSubview:self.timeLabel];
|
||||||
|
[self.contentView addSubview:self.replyButton];
|
||||||
[self.contentView addSubview:self.likeButton];
|
[self.contentView addSubview:self.likeButton];
|
||||||
|
|
||||||
// 左侧缩进 48pt(对齐一级评论内容)
|
// 左侧缩进(对齐一级评论内容)
|
||||||
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.contentView).offset(68); // 16 + 40 + 12 = 68
|
make.left.equalTo(self.contentView).offset(68); // 16 + 40 + 12 = 68
|
||||||
make.top.equalTo(self.contentView).offset(8);
|
make.top.equalTo(self.contentView).offset(8);
|
||||||
make.width.height.mas_equalTo(28);
|
make.width.height.mas_equalTo(28);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.userNameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.avatarImageView.mas_right).offset(8);
|
make.left.equalTo(self.avatarImageView.mas_right).offset(8);
|
||||||
make.top.equalTo(self.avatarImageView);
|
make.top.equalTo(self.avatarImageView);
|
||||||
make.right.equalTo(self.likeButton.mas_left).offset(-8);
|
make.right.equalTo(self.contentView).offset(-50);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.left.equalTo(self.contentLabel);
|
make.left.equalTo(self.userNameLabel);
|
||||||
make.top.equalTo(self.contentLabel.mas_bottom).offset(4);
|
make.top.equalTo(self.contentLabel.mas_bottom).offset(6);
|
||||||
// 降低优先级,避免与 TableView 设置的固定高度约束冲突
|
|
||||||
make.bottom.equalTo(self.contentView).offset(-8).priority(MASLayoutPriorityDefaultHigh);
|
make.bottom.equalTo(self.contentView).offset(-8).priority(MASLayoutPriorityDefaultHigh);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
[self.replyButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.timeLabel.mas_right).offset(16);
|
||||||
|
make.centerY.equalTo(self.timeLabel);
|
||||||
|
}];
|
||||||
|
|
||||||
[self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.right.equalTo(self.contentView).offset(-16);
|
make.right.equalTo(self.contentView).offset(-16);
|
||||||
make.top.equalTo(self.contentView).offset(8);
|
make.top.equalTo(self.contentView).offset(8);
|
||||||
make.width.mas_equalTo(40);
|
make.width.mas_equalTo(30);
|
||||||
make.height.mas_equalTo(30);
|
make.height.mas_equalTo(30);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(self.userNameLabel);
|
||||||
|
make.top.equalTo(self.userNameLabel.mas_bottom).offset(4);
|
||||||
|
make.right.equalTo(self.likeButton.mas_left).offset(-5);
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Configuration
|
#pragma mark - Configuration
|
||||||
@@ -75,58 +90,54 @@
|
|||||||
sd_setImageWithURL:[NSURL URLWithString:reply.avatarUrl]
|
sd_setImageWithURL:[NSURL URLWithString:reply.avatarUrl]
|
||||||
placeholderImage:[UIImage imageNamed:@"default_avatar"]];
|
placeholderImage:[UIImage imageNamed:@"default_avatar"]];
|
||||||
|
|
||||||
// 构建富文本:用户名(蓝色)+ @回复用户(蓝色)+ 内容
|
// 用户名(如果有回复对象,显示 "用户名 回复 @被回复用户")
|
||||||
NSMutableAttributedString *attrText =
|
|
||||||
[[NSMutableAttributedString alloc] init];
|
|
||||||
|
|
||||||
// 用户名
|
|
||||||
NSDictionary *nameAttrs = @{
|
|
||||||
NSFontAttributeName : [UIFont systemFontOfSize:14
|
|
||||||
weight:UIFontWeightMedium],
|
|
||||||
NSForegroundColorAttributeName : [UIColor labelColor]
|
|
||||||
};
|
|
||||||
[attrText appendAttributedString:[[NSAttributedString alloc]
|
|
||||||
initWithString:reply.userName
|
|
||||||
attributes:nameAttrs]];
|
|
||||||
|
|
||||||
// @回复用户
|
|
||||||
if (reply.replyToUserName.length > 0) {
|
if (reply.replyToUserName.length > 0) {
|
||||||
|
NSMutableAttributedString *attrName = [[NSMutableAttributedString alloc] init];
|
||||||
|
|
||||||
|
NSDictionary *nameAttrs = @{
|
||||||
|
NSFontAttributeName : [UIFont systemFontOfSize:13 weight:UIFontWeightMedium],
|
||||||
|
NSForegroundColorAttributeName : [UIColor secondaryLabelColor]
|
||||||
|
};
|
||||||
|
[attrName appendAttributedString:[[NSAttributedString alloc]
|
||||||
|
initWithString:reply.userName
|
||||||
|
attributes:nameAttrs]];
|
||||||
|
|
||||||
NSDictionary *replyAttrs = @{
|
NSDictionary *replyAttrs = @{
|
||||||
NSFontAttributeName : [UIFont systemFontOfSize:14],
|
NSFontAttributeName : [UIFont systemFontOfSize:13],
|
||||||
|
NSForegroundColorAttributeName : [UIColor secondaryLabelColor]
|
||||||
|
};
|
||||||
|
[attrName appendAttributedString:[[NSAttributedString alloc]
|
||||||
|
initWithString:@" 回复 "
|
||||||
|
attributes:replyAttrs]];
|
||||||
|
|
||||||
|
NSDictionary *toUserAttrs = @{
|
||||||
|
NSFontAttributeName : [UIFont systemFontOfSize:13],
|
||||||
NSForegroundColorAttributeName : [UIColor systemBlueColor]
|
NSForegroundColorAttributeName : [UIColor systemBlueColor]
|
||||||
};
|
};
|
||||||
NSString *replyTo =
|
[attrName appendAttributedString:[[NSAttributedString alloc]
|
||||||
[NSString stringWithFormat:@" 回复 @%@", reply.replyToUserName];
|
initWithString:[NSString stringWithFormat:@"@%@", reply.replyToUserName]
|
||||||
[attrText appendAttributedString:[[NSAttributedString alloc]
|
attributes:toUserAttrs]];
|
||||||
initWithString:replyTo
|
|
||||||
attributes:replyAttrs]];
|
self.userNameLabel.attributedText = attrName;
|
||||||
|
} else {
|
||||||
|
self.userNameLabel.attributedText = nil;
|
||||||
|
self.userNameLabel.text = reply.userName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内容
|
// 评论内容
|
||||||
NSDictionary *contentAttrs = @{
|
self.contentLabel.text = reply.content;
|
||||||
NSFontAttributeName : [UIFont systemFontOfSize:14],
|
|
||||||
NSForegroundColorAttributeName : [UIColor labelColor]
|
|
||||||
};
|
|
||||||
NSString *content = [NSString stringWithFormat:@":%@", reply.content];
|
|
||||||
[attrText appendAttributedString:[[NSAttributedString alloc]
|
|
||||||
initWithString:content
|
|
||||||
attributes:contentAttrs]];
|
|
||||||
|
|
||||||
self.contentLabel.attributedText = attrText;
|
|
||||||
|
|
||||||
// 时间
|
// 时间
|
||||||
self.timeLabel.text = [reply formattedTime];
|
self.timeLabel.text = [reply formattedTime];
|
||||||
|
|
||||||
// 点赞
|
// 点赞
|
||||||
NSString *likeText =
|
NSString *likeText = reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @"";
|
||||||
reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @"";
|
self.likeButton.textLabel.text = likeText;
|
||||||
[self.likeButton setTitle:likeText forState:UIControlStateNormal];
|
|
||||||
|
|
||||||
UIImage *likeImage = reply.isLiked ? [UIImage systemImageNamed:@"heart.fill"]
|
UIImage *likeImage = reply.isLiked ? [UIImage systemImageNamed:@"heart.fill"]
|
||||||
: [UIImage systemImageNamed:@"heart"];
|
: [UIImage systemImageNamed:@"heart"];
|
||||||
[self.likeButton setImage:likeImage forState:UIControlStateNormal];
|
self.likeButton.iconView.image = likeImage;
|
||||||
self.likeButton.tintColor =
|
self.likeButton.iconView.tintColor = reply.isLiked ? [UIColor systemRedColor] : [UIColor grayColor];
|
||||||
reply.isLiked ? [UIColor systemRedColor] : [UIColor grayColor];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)formatLikeCount:(NSInteger)count {
|
- (NSString *)formatLikeCount:(NSInteger)count {
|
||||||
@@ -146,6 +157,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)replyButtonTapped {
|
||||||
|
if (self.onReplyAction) {
|
||||||
|
self.onReplyAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Lazy Loading
|
#pragma mark - Lazy Loading
|
||||||
|
|
||||||
- (UIImageView *)avatarImageView {
|
- (UIImageView *)avatarImageView {
|
||||||
@@ -159,9 +176,21 @@
|
|||||||
return _avatarImageView;
|
return _avatarImageView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UILabel *)userNameLabel {
|
||||||
|
if (!_userNameLabel) {
|
||||||
|
_userNameLabel = [[UILabel alloc] init];
|
||||||
|
_userNameLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
|
||||||
|
_userNameLabel.textColor = [UIColor secondaryLabelColor];
|
||||||
|
_userNameLabel.numberOfLines = 0;
|
||||||
|
}
|
||||||
|
return _userNameLabel;
|
||||||
|
}
|
||||||
|
|
||||||
- (UILabel *)contentLabel {
|
- (UILabel *)contentLabel {
|
||||||
if (!_contentLabel) {
|
if (!_contentLabel) {
|
||||||
_contentLabel = [[UILabel alloc] init];
|
_contentLabel = [[UILabel alloc] init];
|
||||||
|
_contentLabel.font = [UIFont systemFontOfSize:14];
|
||||||
|
_contentLabel.textColor = [UIColor labelColor];
|
||||||
_contentLabel.numberOfLines = 0;
|
_contentLabel.numberOfLines = 0;
|
||||||
}
|
}
|
||||||
return _contentLabel;
|
return _contentLabel;
|
||||||
@@ -176,22 +205,31 @@
|
|||||||
return _timeLabel;
|
return _timeLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (UIButton *)likeButton {
|
- (UIButton *)replyButton {
|
||||||
|
if (!_replyButton) {
|
||||||
|
_replyButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
|
_replyButton.titleLabel.font = [UIFont systemFontOfSize:11];
|
||||||
|
[_replyButton setTitle:@"回复" forState:UIControlStateNormal];
|
||||||
|
[_replyButton setTitleColor:[UIColor secondaryLabelColor] forState:UIControlStateNormal];
|
||||||
|
[_replyButton addTarget:self
|
||||||
|
action:@selector(replyButtonTapped)
|
||||||
|
forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
|
return _replyButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (KBTopImageButton *)likeButton {
|
||||||
if (!_likeButton) {
|
if (!_likeButton) {
|
||||||
_likeButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
_likeButton = [[KBTopImageButton alloc] init];
|
||||||
_likeButton.titleLabel.font = [UIFont systemFontOfSize:11];
|
_likeButton.iconSize = CGSizeMake(16, 16);
|
||||||
[_likeButton setTitleColor:[UIColor grayColor]
|
_likeButton.spacing = 2;
|
||||||
forState:UIControlStateNormal];
|
_likeButton.iconView.image = [UIImage systemImageNamed:@"heart"];
|
||||||
[_likeButton setImage:[UIImage systemImageNamed:@"heart"]
|
_likeButton.iconView.tintColor = [UIColor grayColor];
|
||||||
forState:UIControlStateNormal];
|
_likeButton.textLabel.font = [UIFont systemFontOfSize:10];
|
||||||
_likeButton.tintColor = [UIColor grayColor];
|
_likeButton.textLabel.textColor = [UIColor grayColor];
|
||||||
[_likeButton addTarget:self
|
[_likeButton addTarget:self
|
||||||
action:@selector(likeButtonTapped)
|
action:@selector(likeButtonTapped)
|
||||||
forControlEvents:UIControlEventTouchUpInside];
|
forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
|
||||||
// 设置图片和文字间距
|
|
||||||
_likeButton.imageEdgeInsets = UIEdgeInsetsMake(0, -2, 0, 2);
|
|
||||||
_likeButton.titleEdgeInsets = UIEdgeInsetsMake(0, 2, 0, -2);
|
|
||||||
}
|
}
|
||||||
return _likeButton;
|
return _likeButton;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user