1
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user