This commit is contained in:
2025-12-17 19:56:22 +08:00
parent 886de394d0
commit 904a6c932a
9 changed files with 16 additions and 8 deletions

View File

@@ -0,0 +1,215 @@
//
// 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<NSDictionary *> *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<NSString *> *)titles images:(NSArray<UIImage *> *)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<NSNumber *> *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