// // KBKeyboardSubscriptionFeatureMarqueeView.m // CustomKeyboard // #import "KBKeyboardSubscriptionFeatureMarqueeView.h" #import "KBKeyboardSubscriptionFeatureItemView.h" #import "Masonry.h" @interface KBKeyboardSubscriptionFeatureMarqueeView () @property (nonatomic, strong) UIScrollView *scrollView; @property (nonatomic, strong) UIView *contentView; @property (nonatomic, strong) CADisplayLink *displayLink; @property (nonatomic, assign) CGFloat loopWidth; @property (nonatomic, copy) NSArray *items; @end @implementation KBKeyboardSubscriptionFeatureMarqueeView static const CGFloat kKBFeatureMarqueeItemSpacing = 12.0; static const CGFloat kKBFeatureMarqueeSpeedPerFrame = 0.35f; - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor clearColor]; [self addSubview:self.scrollView]; [self.scrollView addSubview:self.contentView]; [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self); }]; [self.contentView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.equalTo(self.scrollView); make.left.equalTo(self.scrollView); make.height.equalTo(self.scrollView); make.right.equalTo(self.scrollView); }]; } return self; } - (void)dealloc { [self stopTicker]; } - (void)didMoveToWindow { [super didMoveToWindow]; if (self.window) { [self startTickerIfNeeded]; } else { [self stopTicker]; } } - (void)setHidden:(BOOL)hidden { BOOL oldHidden = self.isHidden; [super setHidden:hidden]; if (oldHidden == hidden) { return; } if (hidden) { [self stopTicker]; } else if (self.window) { [self startTickerIfNeeded]; } } - (void)layoutSubviews { [super layoutSubviews]; // 宽度变化时重新评估是否需要滚动 [self rebuildIfNeeded]; } #pragma mark - Public - (void)configureWithTitles:(NSArray *)titles images:(NSArray *)images { NSInteger count = MIN(titles.count, images.count); if (count <= 0) { self.items = @[]; [self rebuildIfNeeded]; return; } NSMutableArray *arr = [NSMutableArray arrayWithCapacity:(NSUInteger)count]; for (NSInteger i = 0; i < count; i++) { NSString *t = titles[(NSUInteger)i] ?: @""; UIImage *img = images[(NSUInteger)i] ?: [UIImage new]; [arr addObject:@{@"title": t, @"image": img}]; } self.items = [arr copy]; [self rebuildIfNeeded]; } #pragma mark - Build - (void)rebuildIfNeeded { [self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; if (self.items.count == 0) { self.loopWidth = 0; self.scrollView.contentSize = CGSizeZero; self.scrollView.contentOffset = CGPointZero; [self stopTicker]; return; } BOOL shouldLoop = (self.items.count > 1); NSInteger baseCount = self.items.count; NSMutableArray *baseWidths = [NSMutableArray arrayWithCapacity:(NSUInteger)baseCount]; CGFloat baseTotalWidth = 0; for (NSInteger i = 0; i < baseCount; i++) { NSDictionary *info = self.items[(NSUInteger)i]; NSString *title = [info[@"title"] isKindOfClass:NSString.class] ? info[@"title"] : @""; CGFloat w = [KBKeyboardSubscriptionFeatureItemView preferredWidthForTitle:title]; [baseWidths addObject:@(w)]; baseTotalWidth += w; if (i > 0) { baseTotalWidth += kKBFeatureMarqueeItemSpacing; } } NSArray *loopData = shouldLoop ? [self.items arrayByAddingObjectsFromArray:self.items] : self.items; CGFloat totalWidth = shouldLoop ? (baseTotalWidth * 2 + kKBFeatureMarqueeItemSpacing) : baseTotalWidth; UIView *previous = nil; for (NSInteger idx = 0; idx < loopData.count; idx++) { NSDictionary *info = loopData[(NSUInteger)idx]; UIImage *img = [info[@"image"] isKindOfClass:UIImage.class] ? info[@"image"] : nil; NSString *title = [info[@"title"] isKindOfClass:NSString.class] ? info[@"title"] : @""; CGFloat width = baseWidths[(NSUInteger)(idx % baseCount)].doubleValue; KBKeyboardSubscriptionFeatureItemView *itemView = [[KBKeyboardSubscriptionFeatureItemView alloc] init]; [itemView configureWithImage:(img ?: [UIImage new]) title:title]; [self.contentView addSubview:itemView]; [itemView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.equalTo(self.contentView); make.width.mas_equalTo(width); if (previous) { make.left.equalTo(previous.mas_right).offset(kKBFeatureMarqueeItemSpacing); } else { make.left.equalTo(self.contentView.mas_left); } }]; previous = itemView; } [self.contentView mas_remakeConstraints:^(MASConstraintMaker *make) { make.top.bottom.equalTo(self.scrollView); make.left.equalTo(self.scrollView); make.height.equalTo(self.scrollView); if (previous) { make.right.equalTo(previous.mas_right); } else { make.right.equalTo(self.scrollView); } }]; CGFloat minWidth = CGRectGetWidth(self.bounds); if (minWidth <= 0) { minWidth = 1; } CGFloat height = CGRectGetHeight(self.bounds); if (height <= 0) { height = 48; } CGFloat contentWidth = totalWidth; if (contentWidth <= minWidth) { contentWidth = minWidth; self.loopWidth = 0; [self stopTicker]; self.scrollView.contentOffset = CGPointZero; } else { self.loopWidth = shouldLoop ? (baseTotalWidth + kKBFeatureMarqueeItemSpacing) : 0; [self startTickerIfNeeded]; } self.scrollView.contentSize = CGSizeMake(contentWidth, height); } #pragma mark - Ticker - (void)startTickerIfNeeded { if (self.displayLink || self.loopWidth <= 0) { return; } self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleTick)]; self.displayLink.preferredFramesPerSecond = 60; [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } - (void)stopTicker { [self.displayLink invalidate]; self.displayLink = nil; } - (void)handleTick { if (self.loopWidth <= 0) { return; } CGFloat nextX = self.scrollView.contentOffset.x + kKBFeatureMarqueeSpeedPerFrame; if (nextX >= self.loopWidth) { nextX -= self.loopWidth; } self.scrollView.contentOffset = CGPointMake(nextX, 0); } #pragma mark - Lazy - (UIScrollView *)scrollView { if (!_scrollView) { _scrollView = [[UIScrollView alloc] init]; _scrollView.showsHorizontalScrollIndicator = NO; _scrollView.scrollEnabled = NO; _scrollView.clipsToBounds = YES; } return _scrollView; } - (UIView *)contentView { if (!_contentView) { _contentView = [[UIView alloc] init]; } return _contentView; } @end