ChatPopoverView.m 9.7 KB

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