diff --git a/DOC/新Banner组件架构设计.md b/DOC/新Banner组件架构设计.md new file mode 100644 index 00000000..083f084c --- /dev/null +++ b/DOC/新Banner组件架构设计.md @@ -0,0 +1,472 @@ +# 新Banner组件架构设计 + +## 设计概述 + +基于对现有7种Banner组件的深度分析,设计一套统一的Banner组件架构,包含父类、子类、数据模型和用户反馈机制。 + +## 现有组件分析总结 + +### 共同UI结构模式 + +| 组件 | 背景 | 头像 | 标题/内容 | 礼物图标 | 操作按钮 | 动画效果 | +|------|------|------|-----------|----------|----------|----------| +| RoomHighValueGiftBannerAnimation | ✓ | ✓ | ✓ | ✓ | ✓ | SVGA | +| CPGiftBanner | ✓ | ✓✓(双人) | ✓ | ✓ | - | POP | +| BravoGiftBannerView | ✓ | ✓ | ✓ | ✓ | - | SVGA | +| LuckyPackageBannerView | ✓ | ✓ | ✓ | - | ✓ | POP | +| LuckyGiftWinningBannerView | ✓ | ✓ | ✓ | - | ✓ | POP | +| GameUniversalBannerView | ✓ | ✓ | ✓ | ✓ | ✓ | SVGA | +| PIUniversalBannerView | ✓ | - | ✓ | - | ✓ | SVGA | + +### 共同数据模式 + +- AttachmentModel作为数据源 +- 专用ViewModel进行数据解析 +- 完成回调机制 +- 用户交互跳转 + +## 架构设计 + +### 1. 基础父类设计 + +```objc +// YMBaseBannerView.h +#import +#import "YMBannerDataProtocol.h" +#import "YMBannerDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@class AttachmentModel, YMBaseBannerViewModel; + +typedef NS_ENUM(NSUInteger, YMBannerType) { + YMBannerTypeHighValueGift, // 高价值礼物 + YMBannerTypeCPGift, // CP礼物 + YMBannerTypeBravoGift, // Bravo超级礼物 + YMBannerTypeLuckyPackage, // 幸运红包 + YMBannerTypeLuckyWinning, // 幸运中奖 + YMBannerTypeGameUniversal, // 通用游戏 + YMBannerTypeUniversal // 通用飘屏 +}; + +typedef NS_ENUM(NSUInteger, YMBannerAnimationType) { + YMBannerAnimationTypeSlide, // 滑动动画 + YMBannerAnimationTypeFade, // 淡入淡出 + YMBannerAnimationTypeBounce, // 弹跳效果 + YMBannerAnimationTypeCustom // 自定义动画 +}; + +@interface YMBaseBannerView : UIView + +#pragma mark - 核心属性 +@property (nonatomic, assign, readonly) YMBannerType bannerType; +@property (nonatomic, strong) AttachmentModel *attachment; +@property (nonatomic, strong) YMBaseBannerViewModel *viewModel; +@property (nonatomic, weak) id delegate; + +#pragma mark - UI组件 (子类可选择使用) +@property (nonatomic, strong, readonly) UIView *containerView; +@property (nonatomic, strong, readonly) UIImageView *backgroundImageView; +@property (nonatomic, strong, readonly) NetImageView *avatarImageView; +@property (nonatomic, strong, readonly) NetImageView *secondAvatarImageView; // CP双头像 +@property (nonatomic, strong, readonly) UILabel *titleLabel; +@property (nonatomic, strong, readonly) UILabel *subtitleLabel; +@property (nonatomic, strong, readonly) UILabel *contentLabel; +@property (nonatomic, strong, readonly) NetImageView *iconImageView; +@property (nonatomic, strong, readonly) UIButton *actionButton; +@property (nonatomic, strong, readonly) SVGAImageView *svgaView; + +#pragma mark - 动画配置 +@property (nonatomic, assign) YMBannerAnimationType animationType; +@property (nonatomic, assign) CGFloat showDuration; +@property (nonatomic, assign) CGFloat stayDuration; +@property (nonatomic, assign) CGFloat hideDuration; +@property (nonatomic, assign) BOOL enableSwipeGesture; + +#pragma mark - 回调 +@property (nonatomic, copy) void(^onDisplayComplete)(void); +@property (nonatomic, copy) void(^onUserTap)(YMBaseBannerView *banner); +@property (nonatomic, copy) void(^onActionTap)(YMBaseBannerView *banner); +@property (nonatomic, copy) void(^onDismiss)(YMBaseBannerView *banner); + +#pragma mark - 状态 +@property (nonatomic, assign, readonly) BOOL isDisplaying; +@property (nonatomic, assign, readonly) BOOL isDismissed; + +#pragma mark - 类方法 ++ (instancetype)bannerWithAttachment:(AttachmentModel *)attachment; ++ (void)displayInView:(UIView *)superView + attachment:(AttachmentModel *)attachment + complete:(void(^)(void))complete; + +#pragma mark - 实例方法 +- (instancetype)initWithBannerType:(YMBannerType)type; +- (void)configureWithAttachment:(AttachmentModel *)attachment; +- (void)displayInView:(UIView *)superView; +- (void)dismissWithAnimation:(BOOL)animated; + +#pragma mark - 子类重写方法 +- (Class)viewModelClass; // 返回对应的ViewModel类 +- (void)setupUIComponents; // 设置UI组件 +- (void)layoutUIComponents; // 布局UI组件 +- (void)configureWithViewModel:(YMBaseBannerViewModel *)viewModel; // 配置数据 +- (void)performShowAnimation; // 执行显示动画 +- (void)performHideAnimation; // 执行隐藏动画 +- (void)handleUserTap; // 处理用户点击 +- (void)handleActionTap; // 处理操作按钮点击 + +#pragma mark - 用户反馈 +- (void)reportDisplayEvent; // 上报展示事件 +- (void)reportClickEvent:(NSString *)action; // 上报点击事件 +- (void)reportDismissEvent:(NSString *)reason; // 上报消失事件 + +@end + +NS_ASSUME_NONNULL_END +``` + +### 2. 基础数据模型 + +```objc +// YMBaseBannerViewModel.h +#import +#import "PIBaseModel.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface YMBaseBannerViewModel : PIBaseModel + +#pragma mark - 基础信息 +@property (nonatomic, assign) NSInteger roomUid; +@property (nonatomic, assign) NSInteger targetRoomUid; +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) NSString *subtitle; +@property (nonatomic, copy) NSString *content; + +#pragma mark - 用户信息 +@property (nonatomic, copy) NSString *senderNick; +@property (nonatomic, copy) NSString *senderAvatar; +@property (nonatomic, assign) NSInteger senderUid; +@property (nonatomic, copy) NSString *receiverNick; +@property (nonatomic, copy) NSString *receiverAvatar; +@property (nonatomic, assign) NSInteger receiverUid; + +#pragma mark - 视觉资源 +@property (nonatomic, copy) NSString *backgroundImageUrl; +@property (nonatomic, copy) NSString *iconImageUrl; +@property (nonatomic, copy) NSString *svgaUrl; + +#pragma mark - 交互配置 +@property (nonatomic, copy) NSString *actionText; +@property (nonatomic, copy) NSString *skipUrl; +@property (nonatomic, assign) BOOL enableClick; +@property (nonatomic, assign) BOOL enableAction; + +#pragma mark - 埋点数据 +@property (nonatomic, copy) NSString *eventType; +@property (nonatomic, strong) NSDictionary *trackingData; + +@end + +NS_ASSUME_NONNULL_END +``` + +### 3. 代理协议设计 + +```objc +// YMBannerDelegate.h +#import + +NS_ASSUME_NONNULL_BEGIN + +@class YMBaseBannerView; + +@protocol YMBannerDelegate + +@optional + +#pragma mark - 生命周期回调 +- (void)bannerWillDisplay:(YMBaseBannerView *)banner; +- (void)bannerDidDisplay:(YMBaseBannerView *)banner; +- (void)bannerWillDismiss:(YMBaseBannerView *)banner; +- (void)bannerDidDismiss:(YMBaseBannerView *)banner; + +#pragma mark - 交互回调 +- (void)banner:(YMBaseBannerView *)banner didTapWithAction:(NSString *)action; +- (void)banner:(YMBaseBannerView *)banner willNavigateToRoom:(NSInteger)roomUid; +- (void)banner:(YMBaseBannerView *)banner willOpenURL:(NSString *)url; + +#pragma mark - 数据回调 +- (void)banner:(YMBaseBannerView *)banner didReportEvent:(NSString *)event data:(NSDictionary *)data; + +@end + +NS_ASSUME_NONNULL_END +``` + +## 子类设计 + +### 1. 高价值礼物Banner + +```objc +// YMHighValueGiftBannerView.h +#import "YMBaseBannerView.h" + +@interface YMHighValueGiftBannerView : YMBaseBannerView + +@property (nonatomic, strong, readonly) UILabel *giftNameLabel; +@property (nonatomic, strong, readonly) UILabel *giftCountLabel; +@property (nonatomic, strong, readonly) MarqueeLabel *senderScrollLabel; +@property (nonatomic, strong, readonly) MarqueeLabel *roomNameScrollLabel; + +@end + +// YMHighValueGiftBannerViewModel.h +@interface YMHighValueGiftBannerViewModel : YMBaseBannerViewModel + +@property (nonatomic, copy) NSString *giftName; +@property (nonatomic, assign) NSInteger giftCount; +@property (nonatomic, copy) NSString *giftImageUrl; +@property (nonatomic, assign) NSInteger bgLevel; +@property (nonatomic, copy) NSString *roomTitle; + +@end +``` + +### 2. CP礼物Banner + +```objc +// YMCPGiftBannerView.h +#import "YMBaseBannerView.h" + +@interface YMCPGiftBannerView : YMBaseBannerView + +@property (nonatomic, strong, readonly) UIStackView *cpStackView; +@property (nonatomic, strong, readonly) UILabel *relationLabel; + +@end + +// YMCPGiftBannerViewModel.h +@interface YMCPGiftBannerViewModel : YMBaseBannerViewModel + +@property (nonatomic, copy) NSString *giftImageUrl; +@property (nonatomic, copy) NSString *relationText; + +@end +``` + +### 3. 通用游戏Banner + +```objc +// YMGameUniversalBannerView.h +#import "YMBaseBannerView.h" + +@interface YMGameUniversalBannerView : YMBaseBannerView + +@property (nonatomic, strong, readonly) NetImageView *gameIconView; +@property (nonatomic, assign) NSInteger gameID; + +@end + +// YMGameUniversalBannerViewModel.h +@interface YMGameUniversalBannerViewModel : YMBaseBannerViewModel + +@property (nonatomic, copy) NSString *gameIconUrl; +@property (nonatomic, assign) NSInteger gameID; +@property (nonatomic, copy) NSString *gameTitle; + +@end +``` + +## 用户反馈机制设计 + +### 1. 反馈管理器 + +```objc +// YMBannerFeedbackManager.h +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, YMBannerFeedbackType) { + YMBannerFeedbackTypeDisplay, // 展示 + YMBannerFeedbackTypeClick, // 点击 + YMBannerFeedbackTypeAction, // 操作 + YMBannerFeedbackTypeDismiss, // 消失 + YMBannerFeedbackTypeError // 错误 +}; + +@interface YMBannerFeedbackManager : NSObject + ++ (instancetype)shared; + +#pragma mark - 事件上报 +- (void)reportBannerEvent:(YMBannerFeedbackType)type + banner:(YMBaseBannerView *)banner + data:(NSDictionary *)data; + +#pragma mark - 性能监控 +- (void)startPerformanceMonitoring:(YMBaseBannerView *)banner; +- (void)endPerformanceMonitoring:(YMBaseBannerView *)banner; + +#pragma mark - 错误收集 +- (void)reportError:(NSError *)error + banner:(YMBaseBannerView *)banner + context:(NSDictionary *)context; + +@end + +NS_ASSUME_NONNULL_END +``` + +### 2. 埋点事件定义 + +```objc +// YMBannerTrackingEvents.h +extern NSString * const kYMBannerEventDisplay; // banner_display +extern NSString * const kYMBannerEventClick; // banner_click +extern NSString * const kYMBannerEventAction; // banner_action +extern NSString * const kYMBannerEventDismiss; // banner_dismiss +extern NSString * const kYMBannerEventLoadStart; // banner_load_start +extern NSString * const kYMBannerEventLoadEnd; // banner_load_end +extern NSString * const kYMBannerEventError; // banner_error + +// 埋点参数Key +extern NSString * const kYMBannerTrackingKeyType; // banner_type +extern NSString * const kYMBannerTrackingKeyRoomUid; // room_uid +extern NSString * const kYMBannerTrackingKeyDuration; // duration +extern NSString * const kYMBannerTrackingKeyAction; // action +extern NSString * const kYMBannerTrackingKeyReason; // reason +``` + +## 上下文调整说明 + +### 1. RoomAnimationView.m 调整 + +#### 1.1 替换现有Banner创建逻辑 + +```objc +// 原代码 +- (void)playRoomGiftBanner:(AttachmentModel *)obj { + [RoomHighValueGiftBannerAnimation display:self.bannerContainer + with:obj + complete:^{ + self.isRoomBannerV2Displaying = NO; + [self processNextRoomEffectAttachment]; + }]; +} + +// 新代码 +- (void)playRoomGiftBanner:(AttachmentModel *)obj { + YMHighValueGiftBannerView *banner = [YMHighValueGiftBannerView bannerWithAttachment:obj]; + banner.delegate = self; + banner.onDisplayComplete = ^{ + self.isRoomBannerV2Displaying = NO; + [self processNextRoomEffectAttachment]; + }; + [banner displayInView:self.bannerContainer]; +} +``` + +#### 1.2 添加代理实现 + +```objc +#pragma mark - YMBannerDelegate +- (void)banner:(YMBaseBannerView *)banner didReportEvent:(NSString *)event data:(NSDictionary *)data { + [[YMBannerFeedbackManager shared] reportBannerEvent:YMBannerFeedbackTypeDisplay + banner:banner + data:data]; +} + +- (void)banner:(YMBaseBannerView *)banner willNavigateToRoom:(NSInteger)roomUid { + // 处理房间跳转逻辑 + RoomInfoModel *currentRoom = self.hostDelegate.getRoomInfo; + if (currentRoom.uid != roomUid) { + // 执行跨房间跳转 + } +} +``` + +### 2. 项目依赖调整 + +#### 2.1 新增文件结构 + +``` +YuMi/Modules/YMRoom/View/Banner/ +├── Base/ +│ ├── YMBaseBannerView.h/.m +│ ├── YMBaseBannerViewModel.h/.m +│ └── YMBannerDelegate.h +├── Subclasses/ +│ ├── YMHighValueGiftBannerView.h/.m +│ ├── YMCPGiftBannerView.h/.m +│ ├── YMBravoGiftBannerView.h/.m +│ ├── YMGameUniversalBannerView.h/.m +│ └── YMLuckyPackageBannerView.h/.m +├── Manager/ +│ └── YMBannerFeedbackManager.h/.m +└── Constants/ + └── YMBannerTrackingEvents.h/.m +``` + +#### 2.2 Podfile依赖更新 + +```ruby +# 确保动画库版本兼容 +pod 'pop', '~> 1.0' +pod 'SVGAPlayer', '~> 2.3' + +# 新增性能监控 +pod 'YMPerformanceMonitor' # 如果有自定义性能监控库 +``` + +### 3. 配置文件调整 + +#### 3.1 添加Banner配置 + +```objc +// YMBannerConfig.h +@interface YMBannerConfig : NSObject + +@property (nonatomic, assign) BOOL enablePerformanceMonitoring; +@property (nonatomic, assign) BOOL enableErrorReporting; +@property (nonatomic, assign) CGFloat defaultShowDuration; +@property (nonatomic, assign) CGFloat defaultStayDuration; +@property (nonatomic, assign) CGFloat defaultHideDuration; + ++ (instancetype)shared; + +@end +``` + +### 4. 迁移策略 + +#### 4.1 Phase 1: 基础架构 + +- 创建基类和协议 +- 实现反馈管理器 +- 设置基础配置 + +#### 4.2 Phase 2: 渐进迁移 + +- 优先迁移使用频率最高的Banner +- 保持向后兼容性 +- 添加单元测试 + +#### 4.3 Phase 3: 优化完善 + +- 移除旧代码 +- 性能优化 +- 文档完善 + +## 总结 + +这套新的Banner组件架构具备以下优势: + +1. **统一性**: 提供一致的API和行为模式 +2. **扩展性**: 易于添加新的Banner类型 +3. **可维护性**: 集中管理动画、埋点和反馈 +4. **性能优化**: 统一的资源管理和内存优化 +5. **用户体验**: 标准化的交互和反馈机制 + +通过这套架构,可以显著提升Banner组件的开发效率和用户体验质量。 diff --git a/DOC/飘屏组件分析文档.md b/DOC/飘屏组件分析文档.md new file mode 100644 index 00000000..33f32c42 --- /dev/null +++ b/DOC/飘屏组件分析文档.md @@ -0,0 +1,80 @@ +# 飘屏组件分析文档 + +## 概述 + +本文档详细分析了项目中在 `RoomAnimationView.m` 的 `bannerContainer` 中展示的所有飘屏类型及其触发逻辑。 + +## 飘屏展示机制 + +- **展示容器**: `bannerContainer` (XPRoomAnimationHitView) +- **位置**: 视图层级最顶层,距离顶部导航栏高度 +- **尺寸**: 屏幕宽度 × 180px 高度 +- **管理机制**: 统一队列管理,按优先级顺序播放 + +## 飘屏类型详细表格 + +| 序号 | 飘屏名称 | 类名 | 触发消息类型 | 处理方法 | 业务场景 | 功能描述 | +|-----|---------|------|-------------|---------|----------|---------| +| 1 | 高价值礼物飘屏 | RoomHighValueGiftBannerAnimation | Custom_Message_Sub_Gift_ChannelNotify | playRoomGiftBanner: | 礼物系统 | 展示高价值礼物的全服广播 | +| 2 | CP礼物飘屏 | CPGiftBanner | Custom_Message_Sub_CP_Gift | playCPGiftBanner: | CP系统 | 展示CP相关礼物特效 | +| 3 | Bravo超级礼物飘屏 | BravoGiftBannerView | Custom_Message_Sub_Super_Gift_Banner | playBroveBanner: | 超级礼物 | 展示Bravo超级礼物效果 | +| 4 | 幸运红包飘屏 | LuckyPackageBannerView | Custom_Message_Sub_LuckyPackage | playLuckyPackageBanner: | 红包系统 | 展示房间红包,支持跨房间跳转 | +| 5 | 幸运礼物中奖飘屏 | LuckyGiftWinningBannerView | Custom_Message_Sub_Super_Gift_Winning_Coins_ALL_Room | playLuckyWinningBanner: | 中奖系统 | 展示全服中奖信息 | +| 6 | 通用游戏飘屏 | GameUniversalBannerView | Custom_Message_Sub_General_Floating_Screen_One_Room
Custom_Message_Sub_General_Floating_Screen_All_Room | playGameBanner: | 游戏系统 | 展示游戏相关飘屏,支持跳转 | +| 7 | 塔罗飘屏 | XPRoomTarrowBannerView | Custom_Message_Sub_Tarot_Advanced
Custom_Message_Sub_Tarot_Intermediate | createTarotBannerAnimation: | 塔罗活动 | 展示塔罗相关活动信息 | +| 8 | 星厨房飘屏 | XPRoomStarKitchenBannerView | Custom_Message_Sub_Star_Kitchen_FullScreen | createStarKitchenBannerAnimation: | 厨房活动 | 展示星厨房活动 | +| 9 | 夺宝精灵飘屏 | - | Custom_Message_Sub_Treasure_Fairy_Draw_Gift_L4
Custom_Message_Sub_Treasure_Fairy_Draw_Gift_L5
Custom_Message_Sub_Treasure_Fairy_Convert_L1/L2/L3 | createTreasureFairyBannerAnimation: | 夺宝活动 | 展示夺宝精灵高等级奖励 | +| 10 | 主播小时榜飘屏 | XPRoomAnchorRankBannerView | Custom_Message_Sub_Anchor_Hour_Rank | 创建主播排行榜飘屏 | 排行榜 | 展示主播小时榜信息 | +| 11 | 通用H5飘屏 | XPRoomTarrowBannerView | Custom_Message_Sub_Common_H5_Novice
Custom_Message_Sub_Common_H5_Advanced | createCommonH5BannerAnimation: | H5活动 | 展示通用H5活动飘屏 | +| 12 | 通用飘屏 | PIUniversalBannerView | 多种消息类型 | createGeneralFloatingScreenAnimation: | 通用展示 | 提供通用飘屏展示能力 | + +## 队列管理机制 + +### 核心属性 + +- **队列数组**: `roomBannertModelsQueueV2` (NSMutableArray) +- **状态标记**: `isRoomBannerV2Displaying` (BOOL) + +### 处理流程 + +1. **消息接收**: 调用 `inserBannerModelToQueue:` 将消息加入队列 +2. **优先级排序**: 调用 `sortBannerQueue` 按消息类型的second值排序(数值越小优先级越高) +3. **顺序播放**: 调用 `processNextRoomEffectAttachment` 逐个播放 +4. **播放完成**: 每个飘屏播放完成后,设置 `isRoomBannerV2Displaying = NO` 并处理下一个 + +### 优先级规则 + +飘屏按 `AttachmentModel.second` 值进行排序,确保重要消息优先展示。 + +## 消息类型分类 + +### 礼物相关 + +- Custom_Message_Sub_Gift_ChannelNotify (32) +- Custom_Message_Sub_CP_Gift +- Custom_Message_Sub_Super_Gift_Banner (1066) + +### 红包/福袋相关 + +- Custom_Message_Sub_LuckyPackage (607) +- Custom_Message_Sub_Super_Gift_Winning_Coins_ALL_Room (1063) + +### 游戏相关 + +- Custom_Message_Sub_General_Floating_Screen_One_Room (1071) +- Custom_Message_Sub_General_Floating_Screen_All_Room (1072) + +### 活动相关 + +- Custom_Message_Sub_Tarot_Advanced (714) +- Custom_Message_Sub_Tarot_Intermediate (713) +- Custom_Message_Sub_Star_Kitchen_FullScreen (1042) +- Custom_Message_Sub_Treasure_Fairy_* (9700系列) + +### 排行榜相关 + +- Custom_Message_Sub_Anchor_Hour_Rank (891) + +### 通用相关 + +- Custom_Message_Sub_Common_H5_* (1100系列) diff --git a/DOC/飘屏组件抽象分析.md b/DOC/飘屏组件抽象分析.md new file mode 100644 index 00000000..e054f4a0 --- /dev/null +++ b/DOC/飘屏组件抽象分析.md @@ -0,0 +1,203 @@ +# 飘屏组件抽象分析 + +## 分析概述 + +基于对项目中12种飘屏组件的深入分析,发现这些组件存在高度相似的设计模式和实现结构,具备抽象出统一父类的条件。 + +## 现状分析 + +### 共同特征 + +经过代码分析,发现所有飘屏组件都具有以下共同特征: + +#### 1. 统一的类方法签名模式 + +大部分Banner类都采用了相似的类方法签名: + +``` objc ++ (void)display:(UIView *)superView + with:(AttachmentModel *)attachment + complete:(void(^)(void))complete; + +// 或者带房间信息的版本 ++ (void)display:(UIView *)superView + inRoomUid:(NSInteger)roomUid + with:(AttachmentModel *)attachment + complete:(void(^)(void))complete +exitCurrentRoom:(void(^)(void))exit; +``` + +#### 2. 共同的内部属性结构 + +```objc +@property (nonatomic, strong) SomeViewModel *model; // 数据模型 +@property (nonatomic, strong) UIImageView *backgroundImageView; // 背景图片 +@property (nonatomic, copy) void(^completeDisplay)(void); // 完成回调 +@property (nonatomic, copy) void(^exitCurrentRoom)(void); // 退房回调 +@property (nonatomic, assign) NSInteger currentRoomUid; // 当前房间ID +``` + +#### 3. 相似的动画流程 + +- 从屏幕右侧滑入 (x = KScreenWidth) +- 短暂停留展示 +- 滑出屏幕或淡出 +- 执行完成回调 + +#### 4. 统一的数据处理模式 + +- 都依赖 `AttachmentModel` 作为数据源 +- 都创建对应的ViewModel进行数据解析 +- 都继承自 `PIBaseModel` + +#### 5. 相似的UI组件 + +- 背景图片视图 +- 用户头像 +- 文本标签 +- 可选的SVGA动画视图 +- 可选的交互按钮 + +## 抽象方案设计 + +### 建议的基类结构: `BaseRoomBannerView` + +```objc +@interface BaseRoomBannerView : UIView + +#pragma mark - 核心属性 +@property (nonatomic, strong) AttachmentModel *attachment; +@property (nonatomic, strong) PIBaseModel *viewModel; +@property (nonatomic, assign) NSInteger currentRoomUid; +@property (nonatomic, assign) NSInteger targetRoomUid; + +#pragma mark - UI组件 +@property (nonatomic, strong) UIView *containerView; +@property (nonatomic, strong) UIImageView *backgroundImageView; +@property (nonatomic, strong) NetImageView *avatarImageView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *subTitleLabel; +@property (nonatomic, strong) UIButton *actionButton; +@property (nonatomic, strong) SVGAImageView *svgaView; + +#pragma mark - 动画配置 +@property (nonatomic, assign) CGFloat showDuration; +@property (nonatomic, assign) CGFloat stayDuration; +@property (nonatomic, assign) CGFloat hideDuration; +@property (nonatomic, assign) BOOL useSlideAnimation; +@property (nonatomic, assign) BOOL useFadeAnimation; + +#pragma mark - 回调 +@property (nonatomic, copy) void(^completeDisplay)(void); +@property (nonatomic, copy) void(^exitCurrentRoom)(void); +@property (nonatomic, copy) void(^didTapBanner)(NSInteger roomID); + +#pragma mark - 类方法 ++ (instancetype)createWithAttachment:(AttachmentModel *)attachment + inRoomUid:(NSInteger)roomUid + complete:(void(^)(void))complete + exitCurrentRoom:(void(^)(void))exit; + ++ (void)display:(UIView *)superView + with:(AttachmentModel *)attachment + complete:(void(^)(void))complete; + ++ (void)display:(UIView *)superView + inRoomUid:(NSInteger)roomUid + with:(AttachmentModel *)attachment + complete:(void(^)(void))complete +exitCurrentRoom:(void(^)(void))exit; + +#pragma mark - 实例方法 +- (void)setupWithAttachment:(AttachmentModel *)attachment; +- (void)showInSuperView:(UIView *)superView; +- (void)performShowAnimation; +- (void)performHideAnimation; + +#pragma mark - 子类重写方法 +- (Class)viewModelClass; // 返回对应的ViewModel类 +- (void)setupUI; // 设置UI布局 +- (void)configureWithModel:(PIBaseModel *)model; // 配置数据 +- (void)customizeAnimation; // 自定义动画 +- (void)handleTapAction; // 处理点击事件 + +@end +``` + +### 抽象优势分析 + +#### 1. 代码复用率提升 + +- 消除重复的动画逻辑 +- 统一的数据处理流程 +- 共享的UI组件管理 + +#### 2. 维护成本降低 + +- 集中管理动画参数 +- 统一的回调处理机制 +- 标准化的生命周期管理 + +#### 3. 扩展性增强 + +- 新增飘屏类型只需继承基类 +- 便于添加通用功能(如埋点、性能监控) +- 支持主题切换等全局功能 + +#### 4. 一致性保证 + +- 统一的动画效果和时长 +- 标准化的交互行为 +- 一致的视觉表现 + +### 实现策略 + +#### 阶段一:创建基类 + +1. 提取共同属性和方法 +2. 实现通用动画逻辑 +3. 定义子类接口规范 + +#### 阶段二:重构现有类 + +1. 逐步迁移现有Banner类继承基类 +2. 移除重复代码 +3. 保持向后兼容 + +#### 阶段三:优化和扩展 + +1. 添加性能监控 +2. 实现主题支持 +3. 统一埋点逻辑 + +### 潜在挑战 + +#### 1. 兼容性问题 + +- 现有代码的重构风险 +- 不同Banner的特殊需求差异 +- 第三方依赖的适配 + +#### 2. 性能考虑 + +- 基类功能过于复杂可能影响性能 +- 内存占用的优化 +- 动画性能的平衡 + +#### 3. 维护复杂度 + +- 基类变更可能影响所有子类 +- 需要详细的文档和测试 +- 团队学习成本 + +## 结论 + +**建议进行抽象**:项目中的飘屏组件具有高度的相似性,抽象出基类能够显著提升代码质量和开发效率。建议采用渐进式重构策略,先创建基类和接口规范,再逐步迁移现有实现,确保系统稳定性的同时实现架构优化。 + +## 优先级建议 + +1. **高优先级**:创建 `BaseRoomBannerView` 基类 +2. **中优先级**:重构使用频率最高的Banner类 +3. **低优先级**:添加扩展功能和优化 + +通过这种抽象设计,能够在保持现有功能完整性的同时,为未来的功能扩展和维护奠定良好的架构基础。 diff --git a/YuMi/Modules/YMMessage/Tool/NIMSDKManager.h b/YuMi/Modules/YMMessage/Tool/NIMSDKManager.h index 24d4b323..49e06cc9 100644 --- a/YuMi/Modules/YMMessage/Tool/NIMSDKManager.h +++ b/YuMi/Modules/YMMessage/Tool/NIMSDKManager.h @@ -260,3 +260,4 @@ NS_ASSUME_NONNULL_END + diff --git a/YuMi/Modules/YMMessage/Tool/NIMSDKManager.m b/YuMi/Modules/YMMessage/Tool/NIMSDKManager.m index 1d37d398..b7368e32 100644 --- a/YuMi/Modules/YMMessage/Tool/NIMSDKManager.m +++ b/YuMi/Modules/YMMessage/Tool/NIMSDKManager.m @@ -406,3 +406,4 @@ + diff --git a/YuMi/Modules/YMRoom/Manager/PublicRoomManager.m b/YuMi/Modules/YMRoom/Manager/PublicRoomManager.m index b31ede54..e10a8fed 100644 --- a/YuMi/Modules/YMRoom/Manager/PublicRoomManager.m +++ b/YuMi/Modules/YMRoom/Manager/PublicRoomManager.m @@ -338,6 +338,7 @@ if ([sessionId isEqualToString:self.currentPublicRoomId]) { NSLog(@"PublicRoomManager: 收到公共房间消息: %@", message.text); // 这里可以添加公共房间消息的处理逻辑 + // TODO: 全服广播转移到此类 } } } diff --git a/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m b/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m index 43009588..e03b942d 100644 --- a/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m +++ b/YuMi/Modules/YMRoom/View/AnimationView/RoomAnimationView.m @@ -2242,6 +2242,9 @@ XPRoomGraffitiGiftAnimationViewDelegate - (XPRoomAnimationHitView *)bannerContainer { if (!_bannerContainer) { _bannerContainer = [[XPRoomAnimationHitView alloc] init]; +#if DEBUG + _bannerContainer.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; +#endif } return _bannerContainer; } diff --git a/YuMi/Modules/YMRoom/View/SendGiftView/View/XPSendGiftView.h b/YuMi/Modules/YMRoom/View/SendGiftView/View/XPSendGiftView.h index 8379a3d8..0485cace 100644 --- a/YuMi/Modules/YMRoom/View/SendGiftView/View/XPSendGiftView.h +++ b/YuMi/Modules/YMRoom/View/SendGiftView/View/XPSendGiftView.h @@ -42,6 +42,12 @@ typedef NS_ENUM(NSInteger, SendGiftType) { // 强制重置连击状态 - (void)forceBoomStateReset; +#if DEBUG +// 调试工具 +- (void)simulateComboViewDisappear; +- (void)simulateNetworkFailureDuringCombo; +#endif + @end NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMRoom/View/SendGiftView/View/XPSendGiftView.m b/YuMi/Modules/YMRoom/View/SendGiftView/View/XPSendGiftView.m index c2baada1..1525ad17 100644 --- a/YuMi/Modules/YMRoom/View/SendGiftView/View/XPSendGiftView.m +++ b/YuMi/Modules/YMRoom/View/SendGiftView/View/XPSendGiftView.m @@ -184,6 +184,40 @@ UIKIT_EXTERN NSString * const kFreeGiftCountdownNotification; } } +#pragma mark - 调试工具 + +#if DEBUG +// 模拟连击UI消失异常 +- (void)simulateComboViewDisappear { + NSLog(@"🔴 [调试] 模拟连击UI消失异常"); + + // 模拟连击面板被意外移除 + if (self.comboView && self.comboView.superview) { + [self.comboView removeFromSuperview]; + self.comboView = nil; + NSLog(@"🔴 模拟异常:连击UI已消失但状态未重置"); + NSLog(@" 当前连击状态:%@", [[GiftComboManager sharedManager] isGiftCombing] ? @"进行中" : @"未进行"); + } else { + NSLog(@"⚠️ 当前没有连击面板可以移除"); + } +} + +// 模拟网络异常导致的连击错误 +- (void)simulateNetworkFailureDuringCombo { + NSLog(@"🔴 [调试] 模拟网络异常导致连击错误"); + + // 先确保连击状态开启 + [[GiftComboManager sharedManager] enableToCombo:YES]; + [[GiftComboManager sharedManager] resetCombo]; + + // 模拟网络请求失败,直接调用强制移除 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [[GiftComboManager sharedManager] forceRemove]; + NSLog(@"🔴 已模拟网络异常,触发强制移除"); + }); +} +#endif + - (instancetype)initWithType:(SendGiftType)type uid:(NSString * __nullable)uid{ if (self = [super init]) { self.modalPresentationStyle = UIModalPresentationOverFullScreen; @@ -202,6 +236,14 @@ UIKIT_EXTERN NSString * const kFreeGiftCountdownNotification; selector:@selector(handleBoomStateForceReset:) name:kBoomStateForceResetNotification object:nil]; + +#if DEBUG + // 注册调试通知 + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(simulateComboViewDisappear) + name:@"DebugSimulateComboViewDisappear" + object:nil]; +#endif [self initSubViews]; [self initSubViewConstraints]; diff --git a/YuMi/Modules/YMRoom/View/XPRoomViewController.m b/YuMi/Modules/YMRoom/View/XPRoomViewController.m index 513af5ae..3e2d7786 100644 --- a/YuMi/Modules/YMRoom/View/XPRoomViewController.m +++ b/YuMi/Modules/YMRoom/View/XPRoomViewController.m @@ -345,11 +345,10 @@ XPCandyTreeInsufficientBalanceViewDelegate> [self setupForBoom]; [self handleGiftComboCallBack]; + //#if DEBUG -// UIButton *b = [UIButton buttonWithType:UIButtonTypeInfoLight]; -// b.frame = CGRectMake(100, 100, 100, 100); -// [self.view addSubview:b]; -// [b addTarget:self action:@selector(test) forControlEvents:UIControlEventTouchUpInside]; +// // 添加调试工具 +// [self setupDebugButtons]; //#endif } //- (void)test { @@ -564,6 +563,174 @@ XPCandyTreeInsufficientBalanceViewDelegate> } } +#pragma mark - 调试工具 + +#if DEBUG +// 模拟状态不一致异常 +- (void)simulateStateInconsistency { + NSLog(@"🔴 [调试] 模拟状态不一致异常"); + + // 模拟UI隐藏但连击状态不匹配 + dispatch_async(dispatch_get_main_queue(), ^{ + self.sideMenu.hidden = YES; + self.menuContainerView.hidden = YES; + NSLog(@"🔴 已隐藏底部栏和侧栏"); + + // 检查连击状态 + BOOL isCombing = [[GiftComboManager sharedManager] isGiftCombing]; + NSLog(@" 当前连击状态:%@", isCombing ? @"进行中" : @"未进行"); + + if (!isCombing) { + NSLog(@"🔴 检测到状态不一致:UI已隐藏但连击未进行"); + // 触发保护机制 + [self forceBoomStateReset]; + } + }); +} + +// 模拟应用进入后台异常 +- (void)simulateAppEnterBackground { + NSLog(@"🔴 [调试] 模拟应用进入后台异常"); + + // 先启动连击状态 + [[GiftComboManager sharedManager] enableToCombo:YES]; + [[GiftComboManager sharedManager] resetCombo]; + NSLog(@"🔴 已启动连击状态"); + + // 延迟1秒后模拟进入后台 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"🔴 模拟应用进入后台..."); + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidEnterBackgroundNotification + object:nil]; + }); +} + +// 模拟内存警告异常 +- (void)simulateMemoryWarning { + NSLog(@"🔴 [调试] 模拟内存警告异常"); + + // 先启动连击状态 + [[GiftComboManager sharedManager] enableToCombo:YES]; + [[GiftComboManager sharedManager] resetCombo]; + NSLog(@"🔴 已启动连击状态"); + + // 延迟1秒后模拟内存警告 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"🔴 模拟内存警告..."); + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + }); +} + +// 模拟礼物面板UI消失 +- (void)simulateGiftViewUIDisappear { + NSLog(@"🔴 [调试] 模拟礼物面板UI消失"); + + // 通过通知获取当前的礼物面板 + [[NSNotificationCenter defaultCenter] postNotificationName:@"DebugSimulateComboViewDisappear" + object:nil]; +} + +// 模拟网络异常 +- (void)simulateNetworkError { + NSLog(@"🔴 [调试] 模拟网络异常"); + + // 先启动连击状态 + [[GiftComboManager sharedManager] enableToCombo:YES]; + [[GiftComboManager sharedManager] resetCombo]; + NSLog(@"🔴 已启动连击状态"); + + // 延迟1秒后模拟网络错误 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"🔴 模拟网络异常,强制移除连击状态..."); + [[GiftComboManager sharedManager] forceRemove]; + }); +} + +// 启动连击用于测试 +- (void)startComboForTest { + NSLog(@"🟢 [调试] 启动连击用于测试"); + + [[GiftComboManager sharedManager] enableToCombo:YES]; + [[GiftComboManager sharedManager] resetCombo]; + + NSLog(@"✅ 连击状态已启动,可以进行异常测试"); +} + +// 检查当前状态 +- (void)checkCurrentState { + BOOL isCombing = [[GiftComboManager sharedManager] isGiftCombing]; + BOOL sideMenuHidden = self.sideMenu.hidden; + BOOL menuContainerHidden = self.menuContainerView.hidden; + + NSLog(@"📊 [状态检查]"); + NSLog(@" 连击状态:%@", isCombing ? @"进行中" : @"未进行"); + NSLog(@" 侧栏状态:%@", sideMenuHidden ? @"隐藏" : @"显示"); + NSLog(@" 底部栏状态:%@", menuContainerHidden ? @"隐藏" : @"显示"); + + if ((sideMenuHidden || menuContainerHidden) && !isCombing) { + NSLog(@"⚠️ 检测到状态不一致!"); + } else { + NSLog(@"✅ 状态正常"); + } +} + +// 设置调试按钮 +- (void)setupDebugButtons { + // 创建调试按钮容器 + UIView *debugContainer = [[UIView alloc] initWithFrame:CGRectMake(20, 80, 280, 260)]; + debugContainer.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8]; + debugContainer.layer.cornerRadius = 12; + debugContainer.layer.borderWidth = 2; + debugContainer.layer.borderColor = [UIColor orangeColor].CGColor; + [self.view addSubview:debugContainer]; + + // 标题 + UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 5, 280, 25)]; + titleLabel.text = @"🔧 连击状态调试工具"; + titleLabel.textColor = [UIColor orangeColor]; + titleLabel.font = [UIFont boldSystemFontOfSize:16]; + titleLabel.textAlignment = NSTextAlignmentCenter; + [debugContainer addSubview:titleLabel]; + + // 创建按钮的通用方法 + void (^createButton)(NSString *, CGRect, SEL) = ^(NSString *title, CGRect frame, SEL action) { + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + [button setTitle:title forState:UIControlStateNormal]; + [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + button.backgroundColor = [[UIColor systemBlueColor] colorWithAlphaComponent:0.7]; + button.layer.cornerRadius = 6; + button.frame = frame; + button.titleLabel.font = [UIFont systemFontOfSize:12]; + [button addTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; + [debugContainer addSubview:button]; + }; + + // 第一行按钮 + createButton(@"启动连击", CGRectMake(10, 35, 80, 30), @selector(startComboForTest)); + createButton(@"检查状态", CGRectMake(100, 35, 80, 30), @selector(checkCurrentState)); + createButton(@"强制重置", CGRectMake(190, 35, 80, 30), @selector(forceBoomStateReset)); + + // 第二行按钮 + createButton(@"状态不一致", CGRectMake(10, 75, 80, 30), @selector(simulateStateInconsistency)); + createButton(@"UI消失", CGRectMake(100, 75, 80, 30), @selector(simulateGiftViewUIDisappear)); + createButton(@"进入后台", CGRectMake(190, 75, 80, 30), @selector(simulateAppEnterBackground)); + + // 第三行按钮 + createButton(@"内存警告", CGRectMake(10, 115, 80, 30), @selector(simulateMemoryWarning)); + createButton(@"网络异常", CGRectMake(100, 115, 80, 30), @selector(simulateNetworkError)); + + // 说明文字 + UILabel *instructionLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 160, 260, 90)]; + instructionLabel.text = @"使用步骤:\n1. 点击\"启动连击\"开始测试\n2. 点击各种异常模拟按钮\n3. 观察控制台日志输出\n4. 检查UI状态是否正确恢复\n5. 点击\"检查状态\"验证结果"; + instructionLabel.textColor = [UIColor lightGrayColor]; + instructionLabel.font = [UIFont systemFontOfSize:11]; + instructionLabel.numberOfLines = 0; + instructionLabel.textAlignment = NSTextAlignmentLeft; + [debugContainer addSubview:instructionLabel]; +} +#endif + #pragma mark - Private Method - (void)initSubViews { self.view.backgroundColor = [UIColor darkGrayColor]; diff --git a/docs/AttachmentModel_Analysis_Report.md b/docs/AttachmentModel_Analysis_Report.md index 123e05f9..7b378fb5 100644 --- a/docs/AttachmentModel_Analysis_Report.md +++ b/docs/AttachmentModel_Analysis_Report.md @@ -422,3 +422,4 @@ if (![partitionId isEqualToString:self.userInfo.partitionId]) { + diff --git a/docs/NIMSDKManager_Usage_Guide.md b/docs/NIMSDKManager_Usage_Guide.md index a40e48b2..f0b71909 100644 --- a/docs/NIMSDKManager_Usage_Guide.md +++ b/docs/NIMSDKManager_Usage_Guide.md @@ -618,3 +618,4 @@ A: 检查APNS证书配置,确保设备Token正确上传。 + diff --git a/docs/NIMSDK_Integration_Documentation.md b/docs/NIMSDK_Integration_Documentation.md index 9a19f792..bd7609a6 100644 --- a/docs/NIMSDK_Integration_Documentation.md +++ b/docs/NIMSDK_Integration_Documentation.md @@ -555,3 +555,4 @@ A: 检查推送消息格式,确保解析逻辑正确。 +