360 lines
13 KiB
Objective-C
360 lines
13 KiB
Objective-C
//
|
||
// AAddHPopView.m
|
||
// AMapNavIOSSDK
|
||
//
|
||
|
||
#import "AAddHPopView.h"
|
||
#import <Masonry/Masonry.h>
|
||
#import "UIColor+ANavMap.h"
|
||
|
||
// ─── 尺寸常量 ────────────────────────────────────────────────────────────────
|
||
static const CGFloat kBubbleWidth = 200.f; // 气泡宽度
|
||
static const CGFloat kHeaderHeight = 50.f; // 顶部绿色标题区高度
|
||
static const CGFloat kOptionHeight = 60.f; // 每行选项高度
|
||
static const CGFloat kArrowWidth = 50.f; // 箭头底边宽(aw = ah*2 使左右45度对称)
|
||
static const CGFloat kArrowHeight = 15.f; // 箭头高度
|
||
static const CGFloat kBubbleRadius = 15.f; // 气泡圆角半径
|
||
static const CGFloat kBubbleGap = 5.f; // 气泡底部与 sourceView 顶部间距
|
||
static const CGFloat kScreenPadding = 15.f; // 气泡距屏幕边缘最小距离
|
||
|
||
|
||
// ─── 自定义气泡容器:自绘白色气泡 + 底部箭头 ─────────────────────────────────
|
||
@interface _ABubbleContainerView : UIView
|
||
/// 箭头中心 X(相对于本 view 坐标系),用于 showInView 精确定位
|
||
@property (nonatomic, assign) CGFloat arrowCenterX;
|
||
@end
|
||
|
||
@implementation _ABubbleContainerView
|
||
|
||
- (instancetype)init {
|
||
self = [super init];
|
||
if (self) {
|
||
self.backgroundColor = [UIColor clearColor];
|
||
_arrowCenterX = kBubbleWidth / 2.f;
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)setArrowCenterX:(CGFloat)arrowCenterX {
|
||
_arrowCenterX = arrowCenterX;
|
||
[self setNeedsDisplay];
|
||
}
|
||
|
||
- (void)drawRect:(CGRect)rect {
|
||
CGFloat bw = kBubbleWidth; // 160
|
||
CGFloat bh = kHeaderHeight + kOptionHeight * 2.f; // 150
|
||
CGFloat r = kBubbleRadius; // 10
|
||
CGFloat aw = kArrowWidth; // 16 箭头底边宽
|
||
CGFloat ah = kArrowHeight; // 10 箭头高度
|
||
|
||
// 箭头在底部偏右:指向右下45度
|
||
// 箭头底边: 从 (arrowX-aw/2, bh) 到 (arrowX, bh)
|
||
// 箭头尖端: (arrowX + ah, bh + ah) — 相对于底边中心向右下45度
|
||
CGFloat arrowX = bw - 15.f; // 箭头底边右端 X(偏右下)
|
||
CGFloat arrowTipX = arrowX + ah; // 箭头尖端 X(向右偏移ah = 45度)
|
||
CGFloat arrowTipY = bh + ah; // 箭头尖端 Y(向下偏移ah = 45度)
|
||
|
||
UIBezierPath *path = [UIBezierPath bezierPath];
|
||
|
||
// ── 顶部左圆角 → 顶部右圆角 ──
|
||
[path moveToPoint:CGPointMake(r, 0)];
|
||
[path addLineToPoint:CGPointMake(bw - r, 0)];
|
||
[path addArcWithCenter:CGPointMake(bw - r, r) radius:r
|
||
startAngle:-M_PI_2 endAngle:0 clockwise:YES];
|
||
|
||
// ── 右边 → 右下圆角 ──
|
||
[path addLineToPoint:CGPointMake(bw, bh - r)];
|
||
[path addArcWithCenter:CGPointMake(bw - r, bh - r) radius:r
|
||
startAngle:0 endAngle:M_PI_2 clockwise:YES];
|
||
|
||
// ── 底边 → 箭头底边右端 ──
|
||
[path addLineToPoint:CGPointMake(arrowX, bh)];
|
||
// 箭头斜边(45度向右下到尖端)
|
||
[path addLineToPoint:CGPointMake(arrowTipX, arrowTipY)];
|
||
// 箭头底边左端(垂直向上)
|
||
[path addLineToPoint:CGPointMake(arrowX - ah, bh)];
|
||
|
||
// ── 左下圆角 ──
|
||
[path addArcWithCenter:CGPointMake(r, bh - r) radius:r
|
||
startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
|
||
|
||
// ── 左边 → 左上圆角 ──
|
||
[path addLineToPoint:CGPointMake(0, r)];
|
||
[path addArcWithCenter:CGPointMake(r, r) radius:r
|
||
startAngle:M_PI endAngle:-M_PI_2 clockwise:YES];
|
||
[path closePath];
|
||
|
||
// ── 阴影 + 白色填充 ──
|
||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||
CGContextSaveGState(ctx);
|
||
CGContextSetShadowWithColor(ctx, CGSizeMake(0, 3), 10.f,
|
||
[[UIColor blackColor] colorWithAlphaComponent:0.15f].CGColor);
|
||
[[UIColor whiteColor] setFill];
|
||
[path fill];
|
||
CGContextRestoreGState(ctx);
|
||
[[UIColor whiteColor] setFill];
|
||
[path fill];
|
||
|
||
// 保存箭头尖端坐标用于定位
|
||
_arrowTipX = arrowTipX;
|
||
_arrowTipY = arrowTipY;
|
||
}
|
||
|
||
// 供外部访问的箭头尖端坐标
|
||
static CGFloat _arrowTipX = 0;
|
||
static CGFloat _arrowTipY = 0;
|
||
|
||
@end
|
||
|
||
|
||
// ─── AAddHPopView ────────────────────────────────────────────────────────────
|
||
|
||
@interface AAddHPopView ()
|
||
|
||
@property (nonatomic, strong) _ABubbleContainerView *bubbleContainer;
|
||
@property (nonatomic, strong) UIView *headerView;
|
||
@property (nonatomic, strong) UILabel *titleLabel;
|
||
@property (nonatomic, strong) UIView *separatorLine1;
|
||
@property (nonatomic, strong) UILabel *option1Label;
|
||
@property (nonatomic, strong) UIView *separatorLine2;
|
||
@property (nonatomic, strong) UILabel *option2Label;
|
||
|
||
@end
|
||
|
||
@implementation AAddHPopView
|
||
|
||
#pragma mark - Init
|
||
|
||
- (instancetype)init {
|
||
return [self initWithFrame:CGRectZero];
|
||
}
|
||
|
||
- (instancetype)initWithFrame:(CGRect)frame {
|
||
self = [super initWithFrame:frame];
|
||
if (self) {
|
||
[self p_setupUI];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
#pragma mark - Setup UI
|
||
|
||
- (void)p_setupUI {
|
||
self.backgroundColor = [UIColor clearColor];
|
||
|
||
// ── 点击空白处(self)消失 ──
|
||
UITapGestureRecognizer *bgTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||
action:@selector(dismiss)];
|
||
bgTap.cancelsTouchesInView = NO;
|
||
[self addGestureRecognizer:bgTap];
|
||
|
||
// ── 气泡容器 ──
|
||
[self addSubview:self.bubbleContainer];
|
||
|
||
// ── header(绿色标题区,仅裁剪上两角) ──
|
||
[self.bubbleContainer addSubview:self.headerView];
|
||
[self.headerView addSubview:self.titleLabel];
|
||
|
||
// ── 分隔线 1 ──
|
||
[self.bubbleContainer addSubview:self.separatorLine1];
|
||
|
||
// ── 选项 1 ──
|
||
[self.bubbleContainer addSubview:self.option1Label];
|
||
|
||
// ── 分隔线 2 ──
|
||
[self.bubbleContainer addSubview:self.separatorLine2];
|
||
|
||
// ── 选项 2 ──
|
||
[self.bubbleContainer addSubview:self.option2Label];
|
||
|
||
// ── 阻止气泡内点击冒泡到 bgTap ──
|
||
UITapGestureRecognizer *bubbleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||
action:@selector(p_bubbleTapped)];
|
||
[self.bubbleContainer addGestureRecognizer:bubbleTap];
|
||
|
||
// ── Masonry 内部约束(基于 bubbleContainer) ──
|
||
// 子视图宽度统一约束到 kBubbleWidth(160),气泡主体实际宽度
|
||
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.left.equalTo(self.bubbleContainer);
|
||
make.width.mas_equalTo(kBubbleWidth);
|
||
make.height.mas_equalTo(kHeaderHeight);
|
||
}];
|
||
|
||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.equalTo(self.headerView);
|
||
}];
|
||
|
||
[self.separatorLine1 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(self.headerView.mas_bottom);
|
||
make.left.mas_equalTo(@0);
|
||
make.width.mas_equalTo(kBubbleWidth);
|
||
make.height.mas_equalTo(0.5);
|
||
}];
|
||
|
||
[self.option1Label mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(self.separatorLine1.mas_bottom);
|
||
make.left.mas_equalTo(@0);
|
||
make.width.mas_equalTo(kBubbleWidth);
|
||
make.height.mas_equalTo(kOptionHeight);
|
||
}];
|
||
|
||
[self.separatorLine2 mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(self.option1Label.mas_bottom);
|
||
make.left.mas_equalTo(@0);
|
||
make.width.mas_equalTo(kBubbleWidth);
|
||
make.height.mas_equalTo(0.5);
|
||
}];
|
||
|
||
[self.option2Label mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.top.equalTo(self.separatorLine2.mas_bottom);
|
||
make.left.mas_equalTo(@0);
|
||
make.width.mas_equalTo(kBubbleWidth);
|
||
make.height.mas_equalTo(kOptionHeight);
|
||
}];
|
||
}
|
||
|
||
#pragma mark - Show / Dismiss
|
||
|
||
- (void)showInView:(UIView *)view sourceView:(UIView *)sourceView {
|
||
self.frame = view.bounds;
|
||
[view addSubview:self];
|
||
|
||
// ── 计算 sourceView 在 view 坐标系中的 frame ──
|
||
CGRect srcFrame = [sourceView convertRect:sourceView.bounds toView:view];
|
||
|
||
// ── 容器尺寸 ──
|
||
CGFloat containerW = kBubbleWidth; // 160
|
||
CGFloat containerH = kHeaderHeight + kOptionHeight * 2.f; // 150
|
||
CGFloat ah = kArrowHeight; // 16 箭头高度
|
||
|
||
// ── 箭头尖端坐标(相对于 bubbleContainer 左下角为原点)──
|
||
// 与 drawRect 中一致:arrowX = bw - 35, arrowTipX = arrowX + ah
|
||
CGFloat arrowX_inContainer = containerW - 25.f; // 箭头底边右端 X
|
||
CGFloat arrowTipX_inContainer = arrowX_inContainer + ah; // 箭头尖端 X(右下45度)
|
||
CGFloat arrowTipY_inContainer = containerH + ah; // 箭头尖端 Y
|
||
|
||
// ── 位置计算:气泡在按钮左上方,箭头尖端指向按钮中心 ──
|
||
CGFloat srcCenterX = CGRectGetMidX(srcFrame);
|
||
CGFloat srcCenterY = CGRectGetMidY(srcFrame);
|
||
|
||
// 箭头尖端应指向按钮中心
|
||
CGFloat containerX = srcCenterX - arrowTipX_inContainer;
|
||
containerX = MAX(kScreenPadding, MIN(containerX, view.bounds.size.width - containerW - kScreenPadding));
|
||
|
||
// 气泡顶部位置
|
||
CGFloat containerY = srcCenterY - arrowTipY_inContainer;
|
||
containerY = MAX(kScreenPadding, containerY);
|
||
|
||
self.bubbleContainer.bounds = CGRectMake(0, 0, containerW, containerH + ah);
|
||
|
||
// 锚点设置在箭头尖端位置(相对于容器),实现箭头精准指向
|
||
self.bubbleContainer.layer.anchorPoint = CGPointMake(arrowTipX_inContainer / containerW,
|
||
arrowTipY_inContainer / (containerH + ah));
|
||
self.bubbleContainer.center = CGPointMake(srcCenterX - 25, srcCenterY - 20);
|
||
|
||
// ── 入场动画 ──
|
||
self.alpha = 0.f;
|
||
self.bubbleContainer.transform = CGAffineTransformMakeScale(0.85f, 0.85f);
|
||
|
||
[UIView animateWithDuration:0.25f
|
||
delay:0.f
|
||
usingSpringWithDamping:0.72f
|
||
initialSpringVelocity:0.3f
|
||
options:UIViewAnimationOptionCurveEaseOut
|
||
animations:^{
|
||
self.alpha = 1.f;
|
||
self.bubbleContainer.transform = CGAffineTransformIdentity;
|
||
} completion:nil];
|
||
}
|
||
|
||
- (void)dismiss {
|
||
[UIView animateWithDuration:0.18f
|
||
delay:0.f
|
||
options:UIViewAnimationOptionCurveEaseIn
|
||
animations:^{
|
||
self.alpha = 0.f;
|
||
self.bubbleContainer.transform = CGAffineTransformMakeScale(0.85f, 0.85f);
|
||
} completion:^(BOOL finished) {
|
||
[self removeFromSuperview];
|
||
}];
|
||
}
|
||
|
||
#pragma mark - Private Actions
|
||
|
||
- (void)p_bubbleTapped {
|
||
// 阻止冒泡,气泡内点击不消失
|
||
}
|
||
|
||
#pragma mark - Lazy Load
|
||
|
||
- (_ABubbleContainerView *)bubbleContainer {
|
||
if (!_bubbleContainer) {
|
||
_bubbleContainer = [[_ABubbleContainerView alloc] init];
|
||
}
|
||
return _bubbleContainer;
|
||
}
|
||
|
||
- (UIView *)headerView {
|
||
if (!_headerView) {
|
||
_headerView = [[UIView alloc] init];
|
||
_headerView.backgroundColor = [UIColor hp_colorWithRGBHex:0x1BA855];
|
||
if (@available(iOS 11.0, *)) {
|
||
_headerView.layer.cornerRadius = kBubbleRadius;
|
||
_headerView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
|
||
_headerView.clipsToBounds = YES;
|
||
}
|
||
}
|
||
return _headerView;
|
||
}
|
||
|
||
- (UILabel *)titleLabel {
|
||
if (!_titleLabel) {
|
||
_titleLabel = [[UILabel alloc] init];
|
||
_titleLabel.text = @"加氢规划模式";
|
||
_titleLabel.textColor = [UIColor whiteColor];
|
||
_titleLabel.font = [UIFont boldSystemFontOfSize:15.f];
|
||
_titleLabel.textAlignment = NSTextAlignmentCenter;
|
||
}
|
||
return _titleLabel;
|
||
}
|
||
|
||
- (UIView *)separatorLine1 {
|
||
if (!_separatorLine1) {
|
||
_separatorLine1 = [[UIView alloc] init];
|
||
_separatorLine1.backgroundColor = [UIColor colorWithWhite:0.88f alpha:1.f];
|
||
}
|
||
return _separatorLine1;
|
||
}
|
||
|
||
- (UILabel *)option1Label {
|
||
if (!_option1Label) {
|
||
_option1Label = [[UILabel alloc] init];
|
||
_option1Label.text = @"送货规划模式";
|
||
_option1Label.textColor = [UIColor hp_colorWithRGBHex:0xC9CDD4];
|
||
_option1Label.font = [UIFont boldSystemFontOfSize:14.f];
|
||
_option1Label.textAlignment = NSTextAlignmentCenter;
|
||
}
|
||
return _option1Label;
|
||
}
|
||
|
||
- (UIView *)separatorLine2 {
|
||
if (!_separatorLine2) {
|
||
_separatorLine2 = [[UIView alloc] init];
|
||
_separatorLine2.backgroundColor = [UIColor colorWithWhite:0.88f alpha:1.f];
|
||
}
|
||
return _separatorLine2;
|
||
}
|
||
|
||
- (UILabel *)option2Label {
|
||
if (!_option2Label) {
|
||
_option2Label = [[UILabel alloc] init];
|
||
_option2Label.text = @"成本计算模式";
|
||
_option2Label.textColor = [UIColor hp_colorWithRGBHex:0xC9CDD4];
|
||
_option2Label.font = [UIFont boldSystemFontOfSize:14.f];
|
||
_option2Label.textAlignment = NSTextAlignmentCenter;
|
||
}
|
||
return _option2Label;
|
||
}
|
||
|
||
@end
|