216 lines
6.9 KiB
Mathematica
216 lines
6.9 KiB
Mathematica
|
|
//
|
||
|
|
// 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
|
||
|
|
|