ChatPopoverView.m 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. //
  2. // ChatPopoverView.m
  3. // AIIM
  4. //
  5. // Created by qitewei on 2025/5/7.
  6. //
  7. #import "ChatPopoverView.h"
  8. static CGFloat const kDefaultArrowHeight = 8.0;
  9. static CGFloat const kDefaultCornerRadius = 5.0;
  10. static CGFloat const kDefaultPopoverMargin = 10.0;
  11. static CGFloat const kDefaultBorderWidth = 1.0;
  12. @interface ChatPopoverView ()
  13. @property (nonatomic, strong) UIView *contentView;
  14. @property (nonatomic, weak) UIView *sourceView;
  15. @property (nonatomic, strong) UIControl *overlayView;
  16. @property (nonatomic, assign) PopoverViewArrowDirection finalArrowDirection;
  17. @end
  18. @implementation ChatPopoverView
  19. #pragma mark - Lifecycle
  20. - (instancetype)initWithContentView:(UIView *)contentView sourceView:(UIView *)sourceView {
  21. self = [super initWithFrame:CGRectZero];
  22. if (self) {
  23. _contentView = contentView;
  24. _sourceView = sourceView;
  25. _shouldDismissOnOutsideTap = YES;
  26. _preferredPosition = PopoverViewPositionAuto;
  27. _arrowDirection = PopoverViewArrowDirectionAuto;
  28. _sourceViewGap = 0;
  29. _showArrow = YES;
  30. _popoverBackgroundColor = [UIColor whiteColor];
  31. _borderColor = [UIColor lightGrayColor];
  32. _borderWidth = 1.0;
  33. _cornerRadius = 5.0;
  34. _arrowHeight = 8.0;
  35. _popoverMargin = 10.0;
  36. [self commonInit];
  37. }
  38. return self;
  39. }
  40. - (void)dealloc {
  41. [self.overlayView removeFromSuperview];
  42. }
  43. #pragma mark - Public Methods
  44. - (void)show {
  45. UIWindow *window = [UIApplication sharedApplication].delegate.window;
  46. // if (!window) {
  47. // window = [[UIApplication sharedApplication].windows firstObject];
  48. // }
  49. // 添加遮罩视图
  50. self.overlayView.frame = window.bounds;
  51. [window addSubview:self.overlayView];
  52. [window addSubview:self];
  53. self.alpha = 0;
  54. self.transform = CGAffineTransformMakeScale(0.8, 0.8);
  55. self.overlayView.alpha = 0;
  56. [UIView animateWithDuration:0.2 animations:^{
  57. self.alpha = 1;
  58. self.transform = CGAffineTransformIdentity;
  59. self.overlayView.alpha = 1;
  60. }];
  61. }
  62. - (void)dismiss {
  63. [UIView animateWithDuration:0.2 animations:^{
  64. self.alpha = 0;
  65. self.transform = CGAffineTransformMakeScale(0.8, 0.8);
  66. self.overlayView.alpha = 0;
  67. } completion:^(BOOL finished) {
  68. [self.overlayView removeFromSuperview];
  69. [self removeFromSuperview];
  70. }];
  71. }
  72. #pragma mark - Private Methods
  73. - (void)commonInit {
  74. self.backgroundColor = [UIColor clearColor];
  75. self.clipsToBounds = NO;
  76. [self addSubview:self.contentView];
  77. // 初始化遮罩视图
  78. _overlayView = [[UIControl alloc] initWithFrame:CGRectZero];
  79. _overlayView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2];
  80. [_overlayView addTarget:self action:@selector(overlayTapped) forControlEvents:UIControlEventTouchUpInside];
  81. // 计算最终弹出方向
  82. [self calculateFinalDirection];
  83. }
  84. - (void)overlayTapped {
  85. if (self.shouldDismissOnOutsideTap) {
  86. [self dismiss];
  87. }
  88. }
  89. - (void)calculateFinalDirection {
  90. if (!self.showArrow) {
  91. self.finalArrowDirection = PopoverViewArrowDirectionNone;
  92. return;
  93. }
  94. CGRect sourceRect = [self.sourceView convertRect:self.sourceView.bounds toView:nil];
  95. CGFloat spaceAbove = CGRectGetMinY(sourceRect);
  96. CGFloat spaceBelow = [UIScreen mainScreen].bounds.size.height - CGRectGetMaxY(sourceRect);
  97. if (self.preferredPosition != PopoverViewPositionAuto) {
  98. if (self.preferredPosition == PopoverViewPositionTop) {
  99. self.finalArrowDirection = PopoverViewArrowDirectionDown;
  100. } else {
  101. self.finalArrowDirection = PopoverViewArrowDirectionUp;
  102. }
  103. } else if (self.arrowDirection != PopoverViewArrowDirectionAuto) {
  104. self.finalArrowDirection = self.arrowDirection;
  105. } else {
  106. if (spaceBelow > spaceAbove) {
  107. self.finalArrowDirection = PopoverViewArrowDirectionUp;
  108. } else {
  109. self.finalArrowDirection = PopoverViewArrowDirectionDown;
  110. }
  111. }
  112. }
  113. #pragma mark - Layout
  114. - (void)layoutSubviews {
  115. [super layoutSubviews];
  116. CGRect sourceRect = [self.sourceView convertRect:self.sourceView.bounds toView:nil];
  117. CGFloat currentArrowHeight = self.showArrow ? self.arrowHeight : 0;
  118. CGFloat popoverWidth = CGRectGetWidth(self.contentView.bounds) + 2 * self.cornerRadius;
  119. CGFloat popoverHeight = CGRectGetHeight(self.contentView.bounds) + currentArrowHeight + 2 * self.cornerRadius;
  120. // 计算x坐标,确保不超出屏幕
  121. CGFloat x = CGRectGetMidX(sourceRect) - popoverWidth / 2;
  122. x = MAX(self.popoverMargin, MIN(x, [UIScreen mainScreen].bounds.size.width - popoverWidth - self.popoverMargin));
  123. // 计算y坐标
  124. CGFloat y;
  125. if (self.finalArrowDirection == PopoverViewArrowDirectionUp) {
  126. y = CGRectGetMaxY(sourceRect) + self.sourceViewGap;
  127. } else if (self.finalArrowDirection == PopoverViewArrowDirectionDown) {
  128. y = CGRectGetMinY(sourceRect) - popoverHeight - self.sourceViewGap;
  129. } else {
  130. // 无箭头时默认显示在下方
  131. y = CGRectGetMaxY(sourceRect) + self.sourceViewGap;
  132. }
  133. // 检查是否超出屏幕边界
  134. if (y < self.popoverMargin) {
  135. y = self.popoverMargin;
  136. } else if (y + popoverHeight > [UIScreen mainScreen].bounds.size.height - self.popoverMargin) {
  137. y = [UIScreen mainScreen].bounds.size.height - popoverHeight - self.popoverMargin;
  138. }
  139. self.frame = CGRectMake(x, y, popoverWidth, popoverHeight);
  140. [self updateContentViewFrame];
  141. }
  142. - (void)updateContentViewFrame {
  143. CGFloat currentArrowHeight = self.showArrow ? self.arrowHeight : 0;
  144. if (self.finalArrowDirection == PopoverViewArrowDirectionUp) {
  145. self.contentView.frame = CGRectMake(self.cornerRadius,
  146. currentArrowHeight + self.cornerRadius,
  147. CGRectGetWidth(self.bounds) - 2 * self.cornerRadius,
  148. CGRectGetHeight(self.bounds) - currentArrowHeight - 2 * self.cornerRadius);
  149. } else {
  150. self.contentView.frame = CGRectMake(self.cornerRadius,
  151. self.cornerRadius,
  152. CGRectGetWidth(self.bounds) - 2 * self.cornerRadius,
  153. CGRectGetHeight(self.bounds) - currentArrowHeight - 2 * self.cornerRadius);
  154. }
  155. }
  156. #pragma mark - Drawing
  157. - (void)drawRect:(CGRect)rect {
  158. [super drawRect:rect];
  159. UIBezierPath *path = [UIBezierPath bezierPath];
  160. path.lineWidth = self.borderWidth;
  161. CGRect contentRect;
  162. CGFloat currentArrowHeight = self.showArrow ? self.arrowHeight : 0;
  163. if (self.finalArrowDirection == PopoverViewArrowDirectionUp) {
  164. contentRect = CGRectMake(0, currentArrowHeight, CGRectGetWidth(rect), CGRectGetHeight(rect) - currentArrowHeight);
  165. } else {
  166. contentRect = CGRectMake(0, 0, CGRectGetWidth(rect), CGRectGetHeight(rect) - currentArrowHeight);
  167. }
  168. // 绘制圆角矩形
  169. [path moveToPoint:CGPointMake(CGRectGetMinX(contentRect) + self.cornerRadius, CGRectGetMinY(contentRect))];
  170. [path addLineToPoint:CGPointMake(CGRectGetMaxX(contentRect) - self.cornerRadius, CGRectGetMinY(contentRect))];
  171. [path addArcWithCenter:CGPointMake(CGRectGetMaxX(contentRect) - self.cornerRadius, CGRectGetMinY(contentRect) + self.cornerRadius)
  172. radius:self.cornerRadius
  173. startAngle:3 * M_PI_2
  174. endAngle:0
  175. clockwise:YES];
  176. [path addLineToPoint:CGPointMake(CGRectGetMaxX(contentRect), CGRectGetMaxY(contentRect) - self.cornerRadius)];
  177. [path addArcWithCenter:CGPointMake(CGRectGetMaxX(contentRect) - self.cornerRadius, CGRectGetMaxY(contentRect) - self.cornerRadius)
  178. radius:self.cornerRadius
  179. startAngle:0
  180. endAngle:M_PI_2
  181. clockwise:YES];
  182. [path addLineToPoint:CGPointMake(CGRectGetMinX(contentRect) + self.cornerRadius, CGRectGetMaxY(contentRect))];
  183. [path addArcWithCenter:CGPointMake(CGRectGetMinX(contentRect) + self.cornerRadius, CGRectGetMaxY(contentRect) - self.cornerRadius)
  184. radius:self.cornerRadius
  185. startAngle:M_PI_2
  186. endAngle:M_PI
  187. clockwise:YES];
  188. [path addLineToPoint:CGPointMake(CGRectGetMinX(contentRect), CGRectGetMinY(contentRect) + self.cornerRadius)];
  189. [path addArcWithCenter:CGPointMake(CGRectGetMinX(contentRect) + self.cornerRadius, CGRectGetMinY(contentRect) + self.cornerRadius)
  190. radius:self.cornerRadius
  191. startAngle:M_PI
  192. endAngle:3 * M_PI_2
  193. clockwise:YES];
  194. // 绘制箭头
  195. if (self.showArrow && self.finalArrowDirection != PopoverViewArrowDirectionNone) {
  196. CGRect sourceRect = [self.sourceView convertRect:self.sourceView.bounds toView:nil];
  197. CGFloat arrowX = CGRectGetMidX(sourceRect) - CGRectGetMinX(self.frame);
  198. arrowX = MAX(self.cornerRadius + self.arrowHeight, MIN(arrowX, CGRectGetWidth(rect) - self.cornerRadius - self.arrowHeight));
  199. if (self.finalArrowDirection == PopoverViewArrowDirectionUp) {
  200. [path moveToPoint:CGPointMake(arrowX - self.arrowHeight, self.arrowHeight)];
  201. [path addLineToPoint:CGPointMake(arrowX, 0)];
  202. [path addLineToPoint:CGPointMake(arrowX + self.arrowHeight, self.arrowHeight)];
  203. } else if (self.finalArrowDirection == PopoverViewArrowDirectionDown) {
  204. [path moveToPoint:CGPointMake(arrowX - self.arrowHeight, CGRectGetHeight(rect) - self.arrowHeight)];
  205. [path addLineToPoint:CGPointMake(arrowX, CGRectGetHeight(rect))];
  206. [path addLineToPoint:CGPointMake(arrowX + self.arrowHeight, CGRectGetHeight(rect) - self.arrowHeight)];
  207. }
  208. }
  209. [self.popoverBackgroundColor setFill];
  210. [self.borderColor setStroke];
  211. [path fill];
  212. [path stroke];
  213. }
  214. @end