| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- //
- // TUIChatPopMenu.m
- // TUIChat
- //
- // Created by harvy on 2021/11/30.
- // Copyright © 2023 Tencent. All rights reserved.
- //
- #import "TUIChatPopMenu.h"
- #import <TIMCommon/TIMDefine.h>
- #import <TUICore/TUIThemeManager.h>
- #import "TUIChatPopActionsView.h"
- #import <TIMCommon/TIMCommonMediator.h>
- #import <TIMCommon/TUIEmojiMeditorProtocol.h>
- #import <TUICore/TUICore.h>
- #import "TUIFaceView.h"
- #define maxColumns 5
- #define kContainerInsets UIEdgeInsetsMake(3, 0, 3, 0)
- #define kActionWidth 54
- #define kActionHeight 65
- #define kActionMargin 5
- #define kSepartorHeight 0.5
- #define kSepartorLRMargin 10
- #define kArrowSize CGSizeMake(15, 10)
- #define kEmojiHeight 44
- @implementation TUIChatPopMenuAction
- - (instancetype)initWithTitle:(NSString *)title image:(UIImage *)image weight:(NSInteger)weight callback:(TUIChatPopMenuActionCallback)callback {
- if (self = [super init]) {
- self.title = title;
- self.image = image;
- self.weight = weight;
- self.callback = callback;
- }
- return self;
- }
- @end
- @interface TUIChatPopMenu () <UIGestureRecognizerDelegate,V2TIMAdvancedMsgListener>
- /**
- * emojiRecent view and emoji secondary page view
- */
- @property(nonatomic, strong) UIView *emojiContainerView;
- @property(nonatomic, strong) UIView *containerView;
- @property(nonatomic, strong) NSMutableArray *actions;
- @property(nonatomic, assign) CGPoint arrawPoint;
- @property(nonatomic, assign) CGFloat adjustHeight;
- @property(nonatomic, strong) NSMutableDictionary *actionCallback;
- @property(nonatomic, strong) CAShapeLayer *arrowLayer;
- @property(nonatomic, assign) CGFloat emojiHeight;
- @property(nonatomic, strong) TUIChatPopActionsView *actionsView;
- @property(nonatomic, assign) BOOL hasEmojiView;
- @end
- @implementation TUIChatPopMenu
- - (void)addAction:(TUIChatPopMenuAction *)action {
- if (action) {
- [self.actions addObject:action];
- }
- }
- - (void)removeAllAction {
- [self.actions removeAllObjects];
- }
- - (void)setArrawPosition:(CGPoint)point adjustHeight:(CGFloat)adjustHeight {
- point = CGPointMake(point.x, point.y - NavBar_Height);
- self.arrawPoint = point;
- self.adjustHeight = adjustHeight;
- }
- - (instancetype)initWithEmojiView:(BOOL)hasEmojiView frame:(CGRect)frame {
- self.hasEmojiView = hasEmojiView;
- return [self initWithFrame:frame];
- }
- - (instancetype)initWithFrame:(CGRect)frame {
- if (self = [super initWithFrame:frame]) {
- UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
- UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
- tap.delegate = self;
- pan.delegate = self;
- [self addGestureRecognizer:tap];
- [self addGestureRecognizer:pan];
- if ([self isAddEmojiView]) {
- self.emojiHeight = kEmojiHeight;
- }
- [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(hideWithAnimation) name:@"kTUIChatPopMenuWillHideNotification" object:nil];
- [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(hideWithAnimation) name:UIKeyboardWillChangeFrameNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThemeChanged) name:TUIDidApplyingThemeChangedNotfication object:nil];
- [[V2TIMManager sharedInstance] addAdvancedMsgListener:self];
- }
- return self;
- }
- - (BOOL)isAddEmojiView {
- return self.hasEmojiView && [TUIChatConfig defaultConfig].enablePopMenuEmojiReactAction;
- }
- - (void)onTap:(UIGestureRecognizer *)tap {
- [self hideWithAnimation];
- }
- - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
- if ([touch.view isDescendantOfView:self.emojiContainerView]) {
- return NO;
- }
- if ([touch.view isDescendantOfView:self.containerView]) {
- return NO;
- }
- if (@available(iOS 17.0, *)) {
- CGPoint touchPoint = [touch locationInView:touch.view.nextResponder];
- CGRect frame = self.targetCell.frame;
- if (CGRectContainsPoint(frame, touchPoint)) {
- return NO;
- }
- }
- return YES;
- }
- - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
- if (@available(iOS 17.0, *)) {
- CGPoint touchPoint = [self.superview convertPoint:point fromView:self];
- CGRect frame = self.targetCell.frame;
- CGRect containerFrame = [self.superview convertRect:self.targetCell.container.frame fromView:self.targetCell];
- // CGRect popFrame1 = [self.superview convertRect:self.emojiContainerView.frame fromView:self];
- CGRect popFrame2 = [self.superview convertRect:self.containerView.frame fromView:self];
- if ( CGRectContainsPoint(popFrame2, touchPoint)) {
- return [super hitTest:point withEvent:event];
- }
- [self.superview convertRect:self.targetCell.container.frame fromView:self.targetCell];
-
- if (CGRectContainsPoint(frame, touchPoint)) {
- if ([self.targetCell respondsToSelector:@selector(textView)]) {
- UITextView *textView = [self.targetCell valueForKey:@"textView"];
- if (CGRectContainsPoint(containerFrame,touchPoint)) {
- if (textView && [textView isKindOfClass:UITextView.class] && !textView.isSelectable) {
- [textView selectAll:self];
- }
- return textView;
- }else {
- if (textView && [textView isKindOfClass:UITextView.class]) {
- [textView selectAll:nil];
- [self hideWithAnimation];
- }
- }
- } else {
- [self hideWithAnimation];
- }
- return [super hitTest:point withEvent:event];
- }
- return [super hitTest:point withEvent:event];
- }
- else {
- return [super hitTest:point withEvent:event];
- }
- }
- - (void)hideWithAnimation {
- [UIView animateWithDuration:0.3
- animations:^{
- self.alpha = 0;
- }
- completion:^(BOOL finished) {
- if (finished) {
- if (self.hideCallback) {
- self.hideCallback();
- }
- [self removeFromSuperview];
- }
- }];
- }
- - (void)hideByClickButton:(UIButton *)button callback:(void (^__nullable)(void))callback {
- [UIView animateWithDuration:0.3
- animations:^{
- self.alpha = 0;
- }
- completion:^(BOOL finished) {
- if (finished) {
- if (callback) {
- callback();
- }
- if (self.hideCallback) {
- self.hideCallback();
- }
- [self removeFromSuperview];
- }
- }];
- }
- - (void)showInView:(UIView *)window {
- if (window == nil) {
- window = UIApplication.sharedApplication.keyWindow;
- }
- self.frame = window.bounds;
- [window addSubview:self];
- [self layoutSubview];
- }
- - (void)layoutSubview {
- self.layer.shadowColor = [UIColor blackColor].CGColor;
- self.layer.shadowRadius = 5;
- self.layer.shadowOpacity = 0.5;
- [self updateActionByRank];
- if ([self isAddEmojiView]) {
- [self prepareEmojiView];
- }
- [self prepareContainerView];
- if ([self isAddEmojiView]) {
- [self setupEmojiSubView];
- }
- [self setupContainerPosition];
- [self updateLayout];
-
- if (isRTL()) {
- [self fitRTLViews];
- }
-
- }
- - (void)fitRTLViews {
- if (self.actionsView) {
- for (UIView *subview in self.actionsView.subviews) {
- if ([subview respondsToSelector:@selector(resetFrameToFitRTL)]) {
- [subview resetFrameToFitRTL];
- }
- }
- }
- }
- - (void)updateActionByRank {
- NSArray *ageSortResultArray = [self.actions sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
- TUIChatPopMenuAction *per1 = obj1;
- TUIChatPopMenuAction *per2 = obj2;
- return per1.weight > per2.weight ? NSOrderedAscending : NSOrderedDescending;
- }];
- NSMutableArray *filterArray = [NSMutableArray arrayWithArray:ageSortResultArray];
- self.actions = [NSMutableArray arrayWithArray:ageSortResultArray];
- }
- - (void)setupContainerPosition {
- /**
- * Calculate the coordinates and correct them, the default arrow points down
- */
- CGFloat minTopBottomMargin = (Is_IPhoneX ? (100) : (0.0));
- CGFloat minLeftRightMargin = 50;
- CGFloat containerW = self.containerView.bounds.size.width;
- CGFloat containerH = self.containerView.bounds.size.height;
- CGFloat upContainerY = self.arrawPoint.y + self.adjustHeight + kArrowSize.height; // The containerY value when arrow points up
- /**
- * The default arrow points down
- */
- CGFloat containerX = self.arrawPoint.x - 0.5 * containerW;
- CGFloat containerY = self.arrawPoint.y - kArrowSize.height - containerH - StatusBar_Height - self.emojiHeight;
- BOOL top = NO; // The direction of arrow, here is down
- CGFloat arrawX = 0.5 * containerW;
- CGFloat arrawY = kArrowSize.height + containerH - 1.5;
- /**
- * Corrected vertical coordinates
- */
- if (containerY < minTopBottomMargin) {
- /**
- * At this time, the container is too high, and it is planned to adjust the direction of the arrow to upward.
- */
- if (upContainerY + containerH + minTopBottomMargin > self.superview.bounds.size.height) {
- /**
- * After adjusting the upward arrow direction, it will cause the entire container to exceed the screen. At this time, the adjustment strategy is
- * changed to: keep the arrow direction downward and move self.arrawPoint
- */
- top = NO;
- self.arrawPoint = CGPointMake(self.arrawPoint.x, self.arrawPoint.y - containerY);
- containerY = self.arrawPoint.y - kArrowSize.height - containerH;
- } else {
- /**
- * Adjust the direction of the arrow to meet the requirements
- */
- top = YES;
- self.arrawPoint = CGPointMake(self.arrawPoint.x, self.arrawPoint.y + self.adjustHeight - StatusBar_Height - 5);
- arrawY = -kArrowSize.height;
- containerY = self.arrawPoint.y + kArrowSize.height;
- }
- }
- /**
- *
- * Corrected horizontal coordinates
- */
- if (containerX < minLeftRightMargin) {
- /**
- * At this time, the container is too close to the left side of the screen and needs to move to the right
- */
- CGFloat offset = (minLeftRightMargin - containerX);
- arrawX = arrawX - offset;
- containerX = containerX + offset;
- if (arrawX < 20) {
- arrawX = 20;
- }
- } else if (containerX + containerW + minLeftRightMargin > self.bounds.size.width) {
- /**
- * At this time, the container is too close to the right side of the screen and needs to be moved to the left
- */
- CGFloat offset = containerX + containerW + minLeftRightMargin - self.bounds.size.width;
- arrawX = arrawX + offset;
- containerX = containerX - offset;
- if (arrawX > containerW - 20) {
- arrawX = containerW - 20;
- }
- }
- self.emojiContainerView.frame = CGRectMake(containerX, containerY, containerW, MAX(self.emojiHeight + containerH, 200));
- self.containerView.frame = CGRectMake(containerX, containerY + self.emojiHeight, containerW, containerH);
- /**
- * Drawing arrow
- */
- self.arrowLayer = [[CAShapeLayer alloc] init];
- self.arrowLayer.path = [self arrawPath:CGPointMake(arrawX, arrawY) directionTop:top].CGPath;
- self.arrowLayer.fillColor = TUIChatDynamicColor(@"chat_pop_menu_bg_color", @"#FFFFFF").CGColor;
- if (top) {
- if (self.emojiContainerView) {
- [self.emojiContainerView.layer addSublayer:self.arrowLayer];
- } else {
- [self.containerView.layer addSublayer:self.arrowLayer];
- }
- } else {
- [self.containerView.layer addSublayer:self.arrowLayer];
- }
- }
- - (void)prepareEmojiView {
- if (self.emojiContainerView) {
- [self.emojiContainerView removeFromSuperview];
- self.emojiContainerView = nil;
- }
- self.emojiContainerView = [[UIView alloc] init];
- [self addSubview:self.emojiContainerView];
- }
- - (void)prepareContainerView {
- if (self.containerView) {
- [self.containerView removeFromSuperview];
- self.containerView = nil;
- }
- self.containerView = [[UIView alloc] init];
- [self addSubview:self.containerView];
- self.actionsView = [[TUIChatPopActionsView alloc] init];
- self.actionsView.backgroundColor = TUIChatDynamicColor(@"chat_pop_menu_bg_color", @"#FFFFFF");
- [self.containerView addSubview:self.actionsView];
- int i = 0;
- for (TUIChatPopMenuAction *action in self.actions) {
- UIButton *actionButton = [self buttonWithAction:action tag:[self.actions indexOfObject:action]];
- [self.actionsView addSubview:actionButton];
- i++;
- if (i == maxColumns && i < self.actions.count) {
- UIView *separtorView = [[UIView alloc] init];
- separtorView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#39393B");
- separtorView.hidden = YES;
- [self.actionsView addSubview:separtorView];
- i = 0;
- }
- }
- /**
- * Calculating the size of container
- */
- int rows = (self.actions.count % maxColumns == 0) ? (int)self.actions.count / maxColumns : (int)(self.actions.count / maxColumns) + 1;
- int columns = self.actions.count < maxColumns ? (int)self.actions.count : maxColumns;
- if ([self isAddEmojiView]) {
- columns = maxColumns;
- }
- CGFloat width = kActionWidth * columns + kActionMargin * (columns + 1) + kContainerInsets.left + kContainerInsets.right;
- CGFloat height = kActionHeight * rows + (rows - 1) * kSepartorHeight + kContainerInsets.top + kContainerInsets.bottom;
- self.emojiContainerView.frame = CGRectMake(0, 0, width, self.emojiHeight + height);
- self.containerView.frame = CGRectMake(0, self.emojiHeight, width, height);
- }
- - (void)setupEmojiSubView {
- [self setupEmojiRecentView];
- [self setupEmojiAdvanceView];
- }
- - (void)setupEmojiRecentView {
- NSDictionary *param = @{TUICore_TUIChatExtension_ChatPopMenuReactRecentView_Delegate : self};
- BOOL isRaiseEmojiExtensionSuccess = [TUICore raiseExtension:TUICore_TUIChatExtension_ChatPopMenuReactRecentView_ClassicExtensionID
- parentView:self.emojiContainerView
- param:param];
- if (!isRaiseEmojiExtensionSuccess) {
- self.emojiHeight = 0;
- }
- }
- - (void)setupEmojiAdvanceView {
- NSDictionary *param = @{TUICore_TUIChatExtension_ChatPopMenuReactRecentView_Delegate : self};
- [TUICore raiseExtension:TUICore_TUIChatExtension_ChatPopMenuReactDetailView_ClassicExtensionID parentView:self.emojiContainerView param:param];
- }
- - (void)updateLayout {
-
- self.actionsView.frame = CGRectMake(0, -0.5, self.containerView.frame.size.width, self.containerView.frame.size.height);
- int columns = self.actions.count < maxColumns ? (int)self.actions.count : maxColumns;
- CGFloat containerWidth = kActionWidth * columns + kActionMargin * (columns + 1) + kContainerInsets.left + kContainerInsets.right;
- int i = 0;
- int currentRow = 0;
- int currentColumn = 0;
- for (UIView *subView in self.actionsView.subviews) {
- if ([subView isKindOfClass:UIButton.class]) {
- currentRow = i / maxColumns;
- currentColumn = i % maxColumns;
- CGFloat x = kContainerInsets.left + (currentColumn + 1) * kActionMargin + currentColumn * kActionWidth;
- CGFloat y = kContainerInsets.top + currentRow * kActionHeight + currentRow * kSepartorHeight;
- subView.frame = CGRectMake(x, y, kActionWidth, kActionHeight);
- i++;
- } else {
- CGFloat y = (currentRow + 1) * kActionHeight + kContainerInsets.top;
- CGFloat width = containerWidth - 2 * kSepartorLRMargin - kContainerInsets.left - kContainerInsets.right;
- subView.frame = CGRectMake(kSepartorLRMargin, y, width, kSepartorHeight);
- }
- }
- }
- - (UIBezierPath *)arrawPath:(CGPoint)point directionTop:(BOOL)top {
- CGSize arrowSize = kArrowSize;
- UIBezierPath *arrowPath = [[UIBezierPath alloc] init];
- [arrowPath moveToPoint:point];
- if (top) {
- [arrowPath addLineToPoint:CGPointMake(point.x + arrowSize.width * 0.5, point.y + arrowSize.height)];
- [arrowPath addLineToPoint:CGPointMake(point.x - arrowSize.width * 0.5, point.y + arrowSize.height)];
- } else {
- [arrowPath addLineToPoint:CGPointMake(point.x + arrowSize.width * 0.5, point.y - arrowSize.height)];
- [arrowPath addLineToPoint:CGPointMake(point.x - arrowSize.width * 0.5, point.y - arrowSize.height)];
- }
- [arrowPath closePath];
- return arrowPath;
- }
- - (UIButton *)buttonWithAction:(TUIChatPopMenuAction *)action tag:(NSInteger)tag {
- UIButton *actionButton = [UIButton buttonWithType:UIButtonTypeCustom];
- [actionButton setTitleColor:TUIChatDynamicColor(@"chat_pop_menu_text_color", @"#444444")
- forState:UIControlStateNormal];
- actionButton.titleLabel.font = [UIFont systemFontOfSize:10.0];
- actionButton.titleLabel.numberOfLines = 2;
- actionButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
- [actionButton setTitle:action.title forState:UIControlStateNormal];
- [actionButton setImage:action.image forState:UIControlStateNormal];
- actionButton.contentMode = UIViewContentModeScaleAspectFit;
- [actionButton addTarget:self action:@selector(buttonHighlightedEnter:) forControlEvents:UIControlEventTouchDown];
- [actionButton addTarget:self action:@selector(buttonHighlightedEnter:) forControlEvents:UIControlEventTouchDragEnter];
- [actionButton addTarget:self action:@selector(buttonHighlightedExit:) forControlEvents:UIControlEventTouchDragExit];
- [actionButton addTarget:self action:@selector(onClick:) forControlEvents:UIControlEventTouchUpInside];
- actionButton.tag = tag;
- CGSize imageSize = CGSizeMake(20, 20);
- CGSize titleSize = actionButton.titleLabel.frame.size;
- CGSize textSize = [actionButton.titleLabel.text sizeWithAttributes:@{NSFontAttributeName : actionButton.titleLabel.font}];
- CGSize frameSize = CGSizeMake(ceilf(textSize.width), ceilf(textSize.height));
- if (titleSize.width + 0.5 < frameSize.width) {
- titleSize.width = frameSize.width;
- }
- titleSize.width = MIN(titleSize.width, 48);
- CGFloat totalHeight = (imageSize.height + titleSize.height + 8);
- actionButton.imageEdgeInsets = UIEdgeInsetsMake(-(totalHeight - imageSize.height), 0.0, 0.0, -titleSize.width);
- actionButton.titleEdgeInsets = UIEdgeInsetsMake(0, -imageSize.width, -(totalHeight - titleSize.height), 0);
- [self.actionCallback setObject:action.callback forKey:@(tag)];
- return actionButton;
- }
- - (void)buttonHighlightedEnter:(UIButton *)sender {
- sender.backgroundColor = TUIChatDynamicColor(@"", @"#006EFF19");
- }
- - (void)buttonHighlightedExit:(UIButton *)sender {
- sender.backgroundColor = [UIColor clearColor];
- }
- - (void)onClick:(UIButton *)button {
- if (![self.actionCallback.allKeys containsObject:@(button.tag)]) {
- [self hideWithAnimation];
- return;
- }
- __weak typeof(self) weakSelf = self;
- [self hideByClickButton:button
- callback:^() {
- __strong typeof(weakSelf) strongSelf = weakSelf;
- TUIChatPopMenuActionCallback callback = [strongSelf.actionCallback objectForKey:@(button.tag)];
- if (callback) {
- callback();
- }
- }];
- }
- - (NSMutableArray *)actions {
- if (_actions == nil) {
- _actions = [NSMutableArray array];
- }
- return _actions;
- }
- - (NSMutableDictionary *)actionCallback {
- if (_actionCallback == nil) {
- _actionCallback = [NSMutableDictionary dictionary];
- }
- return _actionCallback;
- }
- // MARK: V2TIMAdvancedMsgListener
- - (void)onRecvMessageRevoked:(NSString *)msgID operateUser:(V2TIMUserFullInfo *)operateUser reason:(NSString *)reason {
- if ([msgID isEqualToString:self.targetCellData.msgID]) {
- [self hideWithAnimation];
- }
- }
- // MARK: ThemeChanged
- - (void)applyBorderTheme {
- if (_arrowLayer) {
- _arrowLayer.fillColor = TUIChatDynamicColor(@"chat_pop_menu_bg_color", @"#FFFFFF").CGColor;
- }
- }
- - (void)onThemeChanged {
- [self applyBorderTheme];
- }
- @end
|