This commit is contained in:
启星
2025-08-12 14:27:12 +08:00
parent 9d18b353b1
commit 1bd5e77c45
8785 changed files with 978163 additions and 2 deletions

View File

@@ -0,0 +1,29 @@
//
// JXCategoryBaseCell.h
// UI系列测试
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "JXCategoryBaseCellModel.h"
#import "JXCategoryViewAnimator.h"
#import "JXCategoryViewDefines.h"
@interface JXCategoryBaseCell : UICollectionViewCell
@property (nonatomic, strong, readonly) JXCategoryBaseCellModel *cellModel;
@property (nonatomic, strong, readonly) JXCategoryViewAnimator *animator;
- (void)initializeViews NS_REQUIRES_SUPER;
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel NS_REQUIRES_SUPER;
- (BOOL)checkCanStartSelectedAnimation:(JXCategoryBaseCellModel *)cellModel;
- (void)addSelectedAnimationBlock:(JXCategoryCellSelectedAnimationBlock)block;
- (void)startSelectedAnimationIfNeeded:(JXCategoryBaseCellModel *)cellModel;
@end

View File

@@ -0,0 +1,98 @@
//
// JXCategoryBaseCell.m
// UI
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryBaseCell.h"
#import "RTLManager.h"
@interface JXCategoryBaseCell ()
@property (nonatomic, strong) JXCategoryBaseCellModel *cellModel;
@property (nonatomic, strong) JXCategoryViewAnimator *animator;
@property (nonatomic, strong) NSMutableArray <JXCategoryCellSelectedAnimationBlock> *animationBlockArray;
@end
@implementation JXCategoryBaseCell
#pragma mark - Initialize
- (void)dealloc {
[self.animator stop];
}
- (void)prepareForReuse {
[super prepareForReuse];
[self.animator stop];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initializeViews];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self initializeViews];
}
return self;
}
#pragma mark - Public
- (void)initializeViews {
_animationBlockArray = [NSMutableArray array];
[RTLManager horizontalFlipViewIfNeeded:self];
[RTLManager horizontalFlipViewIfNeeded:self.contentView];
}
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel {
self.cellModel = cellModel;
if (cellModel.isSelectedAnimationEnabled) {
[self.animationBlockArray removeLastObject];
if ([self checkCanStartSelectedAnimation:cellModel]) {
self.animator = [[JXCategoryViewAnimator alloc] init];
self.animator.duration = cellModel.selectedAnimationDuration;
} else {
[self.animator stop];
}
}
}
- (BOOL)checkCanStartSelectedAnimation:(JXCategoryBaseCellModel *)cellModel {
BOOL canStartSelectedAnimation = ((cellModel.selectedType == JXCategoryCellSelectedTypeCode) || (cellModel.selectedType == JXCategoryCellSelectedTypeClick));
return canStartSelectedAnimation;
}
- (void)addSelectedAnimationBlock:(JXCategoryCellSelectedAnimationBlock)block {
[self.animationBlockArray addObject:block];
}
- (void)startSelectedAnimationIfNeeded:(JXCategoryBaseCellModel *)cellModel {
if (cellModel.isSelectedAnimationEnabled && [self checkCanStartSelectedAnimation:cellModel]) {
// isTransitionAnimating
cellModel.transitionAnimating = YES;
__weak typeof(self)weakSelf = self;
self.animator.progressCallback = ^(CGFloat percent) {
for (JXCategoryCellSelectedAnimationBlock block in weakSelf.animationBlockArray) {
block(percent);
}
};
self.animator.completeCallback = ^{
cellModel.transitionAnimating = NO;
[weakSelf.animationBlockArray removeAllObjects];
};
[self.animator start];
}
}
@end

View File

@@ -0,0 +1,31 @@
//
// JXCategoryBaseCellModel.h
// UI系列测试
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "JXCategoryViewDefines.h"
@interface JXCategoryBaseCellModel : NSObject
@property (nonatomic, assign) NSUInteger index;
@property (nonatomic, assign) CGFloat cellWidth;
@property (nonatomic, assign) CGFloat cellSpacing;
@property (nonatomic, assign, getter=isSelected) BOOL selected;
@property (nonatomic, assign, getter=isCellWidthZoomEnabled) BOOL cellWidthZoomEnabled;
@property (nonatomic, assign) CGFloat cellWidthNormalZoomScale;
@property (nonatomic, assign) CGFloat cellWidthCurrentZoomScale;
@property (nonatomic, assign) CGFloat cellWidthSelectedZoomScale;
@property (nonatomic, assign, getter=isSelectedAnimationEnabled) BOOL selectedAnimationEnabled;
@property (nonatomic, assign) NSTimeInterval selectedAnimationDuration;
@property (nonatomic, assign) JXCategoryCellSelectedType selectedType;
@property (nonatomic, assign, getter=isTransitionAnimating) BOOL transitionAnimating;
@end

View File

@@ -0,0 +1,13 @@
//
// JXCategoryBaseCellModel.m
// UI
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryBaseCellModel.h"
@implementation JXCategoryBaseCellModel
@end

View File

@@ -0,0 +1,222 @@
//
// JXCategoryView.h
// UI系列测试
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "JXCategoryBaseCell.h"
#import "JXCategoryBaseCellModel.h"
#import "JXCategoryCollectionView.h"
#import "JXCategoryViewDefines.h"
@class JXCategoryBaseView;
@protocol JXCategoryViewListContainer <NSObject>
- (void)setDefaultSelectedIndex:(NSInteger)index;
- (UIScrollView *)contentScrollView;
- (void)reloadData;
- (void)didClickSelectedItemAtIndex:(NSInteger)index;
@end
@protocol JXCategoryViewDelegate <NSObject>
@optional
//为什么会把选中代理分为三个,因为有时候只关心点击选中的,有时候只关心滚动选中的,有时候只关心选中。所以具体情况,使用对应方法。
/**
点击选中或者滚动选中都会调用该方法。适用于只关心选中事件,不关心具体是点击还是滚动选中的。
@param categoryView categoryView对象
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didSelectedItemAtIndex:(NSInteger)index;
/**
点击选中的情况才会调用该方法
@param categoryView categoryView对象
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didClickSelectedItemAtIndex:(NSInteger)index;
/**
滚动选中的情况才会调用该方法
@param categoryView categoryView对象
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didScrollSelectedItemAtIndex:(NSInteger)index;
/**
控制能否点击Item
@param categoryView categoryView对象
@param index 准备点击的index
@return 能否点击
*/
- (BOOL)categoryView:(JXCategoryBaseView *)categoryView canClickItemAtIndex:(NSInteger)index;
/**
正在滚动中的回调
@param categoryView categoryView对象
@param leftIndex 正在滚动中相对位置处于左边的index
@param rightIndex 正在滚动中相对位置处于右边的index
@param ratio 从左往右计算的百分比
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView scrollingFromLeftIndex:(NSInteger)leftIndex toRightIndex:(NSInteger)rightIndex ratio:(CGFloat)ratio;
@end
@interface JXCategoryBaseView : UIView
@property (nonatomic, strong, readonly) JXCategoryCollectionView *collectionView;
@property (nonatomic, strong) NSArray <JXCategoryBaseCellModel *> *dataSource;
@property (nonatomic, weak) id<JXCategoryViewDelegate> delegate;
/**
高封装度的列表容器使用该类可以让列表拥有完成的生命周期、自动同步defaultSelectedIndex、自动调用reloadData。
*/
@property (nonatomic, weak) id<JXCategoryViewListContainer> listContainer;
/**
推荐使用封装度更高的listContainer属性。如果使用contentScrollView请参考`LoadDataListCustomViewController`使用示例。
*/
@property (nonatomic, strong) UIScrollView *contentScrollView;
@property (nonatomic, assign) NSInteger defaultSelectedIndex; //修改初始化的时候默认选择的index
@property (nonatomic, assign, readonly) NSInteger selectedIndex;
@property (nonatomic, assign, getter=isContentScrollViewClickTransitionAnimationEnabled) BOOL contentScrollViewClickTransitionAnimationEnabled; //点击cell进行contentScrollView切换时是否需要动画。默认为YES
@property (nonatomic, assign) CGFloat contentEdgeInsetLeft; //整体内容的左边距默认JXCategoryViewAutomaticDimension等于cellSpacing
@property (nonatomic, assign) CGFloat contentEdgeInsetRight; //整体内容的右边距默认JXCategoryViewAutomaticDimension等于cellSpacing
@property (nonatomic, assign) CGFloat cellWidth; //默认JXCategoryViewAutomaticDimension
@property (nonatomic, assign) CGFloat cellWidthIncrement; //cell宽度补偿。默认0
@property (nonatomic, assign) CGFloat cellSpacing; //cell之间的间距默认20
@property (nonatomic, assign, getter=isAverageCellSpacingEnabled) BOOL averageCellSpacingEnabled; //当collectionView.contentSize.width小于JXCategoryBaseView的宽度是否将cellSpacing均分。默认为YES。
//cell宽度是否缩放
@property (nonatomic, assign, getter=isCellWidthZoomEnabled) BOOL cellWidthZoomEnabled; //默认为NO
@property (nonatomic, assign, getter=isCellWidthZoomScrollGradientEnabled) BOOL cellWidthZoomScrollGradientEnabled; //手势滚动过程中是否需要更新cell的宽度。默认为YES
@property (nonatomic, assign) CGFloat cellWidthZoomScale; //默认1.2cellWidthZoomEnabled为YES才生效
@property (nonatomic, assign, getter=isSelectedAnimationEnabled) BOOL selectedAnimationEnabled; //是否开启点击或代码选中动画。默认为NO。自定义的cell选中动画需要自己实现。仅点击或调用selectItemAtIndex选中才有效滚动选中无效
@property (nonatomic, assign) NSTimeInterval selectedAnimationDuration; //cell选中动画的时间。默认0.25
/**
选中目标index的item
@param index 目标index
*/
- (void)selectItemAtIndex:(NSInteger)index;
/**
初始化的时候无需调用。比如页面初始化之后根据网络接口异步回调回来数据重新配置categoryView需要调用该方法进行刷新。
*/
- (void)reloadData;
/**
重新配置categoryView但是不需要reload listContainer。特殊情况是该方法。
*/
- (void)reloadDataWithoutListContainer;
/**
刷新指定的index的cell
内部会触发`- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index`方法进行cellModel刷新
@param index 指定cell的index
*/
- (void)reloadCellAtIndex:(NSInteger)index;
@end
@interface JXCategoryBaseView (UISubclassingBaseHooks)
/**
获取目标cell当前的frame反应当前真实的frame受到cellWidthSelectedZoomScale的影响。
*/
- (CGRect)getTargetCellFrame:(NSInteger)targetIndex;
/**
获取目标cell的选中时的frame其他cell的状态都当做普通状态处理。
*/
- (CGRect)getTargetSelectedCellFrame:(NSInteger)targetIndex selectedType:(JXCategoryCellSelectedType)selectedType;
- (void)initializeData NS_REQUIRES_SUPER;
- (void)initializeViews NS_REQUIRES_SUPER;
/**
reloadData方法调用重新生成数据源赋值到self.dataSource
*/
- (void)refreshDataSource;
/**
reloadData方法调用根据数据源重新刷新状态
*/
- (void)refreshState NS_REQUIRES_SUPER;
/**
选中某个item时刷新将要选中与取消选中的cellModel
@param selectedCellModel 将要选中的cellModel
@param unselectedCellModel 取消选中的cellModel
*/
- (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel NS_REQUIRES_SUPER;
/**
关联的contentScrollView的contentOffset发生了改变
@param contentOffset 偏移量
*/
- (void)contentOffsetOfContentScrollViewDidChanged:(CGPoint)contentOffset NS_REQUIRES_SUPER;
/**
选中某一个item的时候调用该方法用于子类重载。
如果外部要选中某个index请使用`- (void)selectItemAtIndex:(NSUInteger)index;`
@param index 选中的index
@param selectedType JXCategoryCellSelectedType
@return 返回值为NO表示触发内部某些判断点击了同一个cell子类无需后续操作。
*/
- (BOOL)selectCellAtIndex:(NSInteger)index selectedType:(JXCategoryCellSelectedType)selectedType NS_REQUIRES_SUPER;
/**
reloadData时返回每个cell的宽度
@param index 目标index
@return cellWidth
*/
- (CGFloat)preferredCellWidthAtIndex:(NSInteger)index;
/**
返回自定义cell的class
@return cell class
*/
- (Class)preferredCellClass;
/**
refreshState时调用重置cellModel的状态
@param cellModel 待重置的cellModel
@param index cellModel在数组中的index
*/
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index NS_REQUIRES_SUPER;
@end

View File

@@ -0,0 +1,697 @@
//
// JXCategoryBaseView.m
// UI
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryBaseView.h"
#import "JXCategoryFactory.h"
#import "JXCategoryViewAnimator.h"
#import "RTLManager.h"
struct DelegateFlags {
unsigned int didSelectedItemAtIndexFlag : 1;
unsigned int didClickSelectedItemAtIndexFlag : 1;
unsigned int didScrollSelectedItemAtIndexFlag : 1;
unsigned int canClickItemAtIndexFlag : 1;
unsigned int scrollingFromLeftIndexToRightIndexFlag : 1;
};
@interface JXCategoryBaseView () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout>
@property (nonatomic, strong) JXCategoryCollectionView *collectionView;
@property (nonatomic, assign) struct DelegateFlags delegateFlags;
@property (nonatomic, assign) NSInteger selectedIndex;
@property (nonatomic, assign) CGFloat innerCellSpacing;
@property (nonatomic, assign) CGPoint lastContentViewContentOffset;
@property (nonatomic, strong) JXCategoryViewAnimator *animator;
// indexitem
@property (nonatomic, assign) NSInteger scrollingTargetIndex;
@property (nonatomic, assign, getter=isNeedReloadByBecomeActive) BOOL needReloadByBecomeActive;
@property (nonatomic, assign, getter=isFirstLayoutSubviews) BOOL firstLayoutSubviews;
@property (nonatomic, assign, getter=isNeedConfigAutomaticallyAdjustsScrollViewInsets) BOOL needConfigAutomaticallyAdjustsScrollViewInsets;
@end
@implementation JXCategoryBaseView
- (void)dealloc {
if (self.contentScrollView) {
[self.contentScrollView removeObserver:self forKeyPath:@"contentOffset"];
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
[self.animator stop];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initializeData];
[self initializeViews];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self initializeData];
[self initializeViews];
}
return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
[self configAutomaticallyAdjustsScrollViewInsets:newSuperview];
}
- (void)reloadData {
[self reloadDataWithoutListContainer];
[self.listContainer reloadData];
}
- (void)reloadDataWithoutListContainer {
[self refreshDataSource];
[self refreshState];
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
}
- (void)reloadCellAtIndex:(NSInteger)index {
if (index < 0 || index >= self.dataSource.count) {
return;
}
JXCategoryBaseCellModel *cellModel = self.dataSource[index];
cellModel.selectedType = JXCategoryCellSelectedTypeUnknown;
[self refreshCellModel:cellModel index:index];
JXCategoryBaseCell *cell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[cell reloadData:cellModel];
}
- (void)selectItemAtIndex:(NSInteger)index {
[self selectCellAtIndex:index selectedType:JXCategoryCellSelectedTypeCode];
}
- (void)layoutSubviews {
[super layoutSubviews];
//使JXCategoryViewUICollectionView
//JXCategoryView
CGRect targetFrame = CGRectMake(0, 0, self.bounds.size.width, floor(self.bounds.size.height));
if (self.isFirstLayoutSubviews) {
if (self.bounds.size.width == 0 || self.bounds.size.height == 0) {
return;
}
if (self.isNeedConfigAutomaticallyAdjustsScrollViewInsets) {
[self configAutomaticallyAdjustsScrollViewInsets:self.superview];
}
self.firstLayoutSubviews = NO;
self.collectionView.frame = targetFrame;
[self reloadDataWithoutListContainer];
}else {
if (!CGRectEqualToRect(self.collectionView.frame, targetFrame)) {
self.collectionView.frame = targetFrame;
[self refreshState];
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
}
}
}
#pragma mark - Setter
- (void)setDelegate:(id<JXCategoryViewDelegate>)delegate {
_delegate = delegate;
_delegateFlags.didSelectedItemAtIndexFlag = [delegate respondsToSelector:@selector(categoryView:didSelectedItemAtIndex:)];
_delegateFlags.didClickSelectedItemAtIndexFlag = [delegate respondsToSelector:@selector(categoryView:didClickSelectedItemAtIndex:)];
_delegateFlags.didScrollSelectedItemAtIndexFlag = [delegate respondsToSelector:@selector(categoryView:didScrollSelectedItemAtIndex:)];
_delegateFlags.canClickItemAtIndexFlag = [delegate respondsToSelector:@selector(categoryView:canClickItemAtIndex:)];
_delegateFlags.scrollingFromLeftIndexToRightIndexFlag = [delegate respondsToSelector:@selector(categoryView:scrollingFromLeftIndex:toRightIndex:ratio:)];
}
- (void)setDefaultSelectedIndex:(NSInteger)defaultSelectedIndex {
_defaultSelectedIndex = defaultSelectedIndex;
self.selectedIndex = defaultSelectedIndex;
[self.listContainer setDefaultSelectedIndex:defaultSelectedIndex];
}
- (void)setContentScrollView:(UIScrollView *)contentScrollView {
if (_contentScrollView != nil) {
[_contentScrollView removeObserver:self forKeyPath:@"contentOffset"];
}
_contentScrollView = contentScrollView;
self.contentScrollView.scrollsToTop = NO;
[self.contentScrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)setListContainer:(id<JXCategoryViewListContainer>)listContainer {
_listContainer = listContainer;
[listContainer setDefaultSelectedIndex:self.defaultSelectedIndex];
self.contentScrollView = [listContainer contentScrollView];
}
#pragma mark - <UICollectionViewDataSource, UICollectionViewDelegate>
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.dataSource.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
return [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([self preferredCellClass]) forIndexPath:indexPath];
}
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
JXCategoryBaseCellModel *cellModel = self.dataSource[indexPath.item];
cellModel.selectedType = JXCategoryCellSelectedTypeUnknown;
[(JXCategoryBaseCell *)cell reloadData:cellModel];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
BOOL isTransitionAnimating = NO;
for (JXCategoryBaseCellModel *cellModel in self.dataSource) {
if (cellModel.isTransitionAnimating) {
isTransitionAnimating = YES;
break;
}
}
if (!isTransitionAnimating) {
//item
[self clickSelectItemAtIndex:indexPath.row];
}
}
#pragma mark - <UICollectionViewDelegateFlowLayout>
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0, [self getContentEdgeInsetLeft], 0, [self getContentEdgeInsetRight]);
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(self.dataSource[indexPath.item].cellWidth, self.collectionView.bounds.size.height);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return self.innerCellSpacing;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return self.innerCellSpacing;
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"]) {
CGPoint contentOffset = [change[NSKeyValueChangeNewKey] CGPointValue];
if ((self.contentScrollView.isTracking || self.contentScrollView.isDecelerating)) {
//
[self contentOffsetOfContentScrollViewDidChanged:contentOffset];
}
self.lastContentViewContentOffset = contentOffset;
}
}
#pragma mark - Private
- (void)configAutomaticallyAdjustsScrollViewInsets:(UIView *)view {
UIResponder *next = view;
while (next != nil) {
if ([next isKindOfClass:[UIViewController class]]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
((UIViewController *)next).automaticallyAdjustsScrollViewInsets = NO;
#pragma clang diagnostic pop
self.needConfigAutomaticallyAdjustsScrollViewInsets = NO;
break;
}
next = next.nextResponder;
}
}
- (CGFloat)getContentEdgeInsetLeft {
if (self.contentEdgeInsetLeft == JXCategoryViewAutomaticDimension) {
return self.innerCellSpacing;
}
return self.contentEdgeInsetLeft;
}
- (CGFloat)getContentEdgeInsetRight {
if (self.contentEdgeInsetRight == JXCategoryViewAutomaticDimension) {
return self.innerCellSpacing;
}
return self.contentEdgeInsetRight;
}
- (CGFloat)getCellWidthAtIndex:(NSInteger)index {
return [self preferredCellWidthAtIndex:index] + self.cellWidthIncrement;
}
- (void)clickSelectItemAtIndex:(NSInteger)index {
if (self.delegateFlags.canClickItemAtIndexFlag && ![self.delegate categoryView:self canClickItemAtIndex:index]) {
return;
}
[self selectCellAtIndex:index selectedType:JXCategoryCellSelectedTypeClick];
}
- (void)scrollSelectItemAtIndex:(NSInteger)index {
[self selectCellAtIndex:index selectedType:JXCategoryCellSelectedTypeScroll];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
if (self.isNeedReloadByBecomeActive) {
self.needReloadByBecomeActive = NO;
[self reloadData];
}
}
@end
@implementation JXCategoryBaseView (UISubclassingBaseHooks)
- (CGRect)getTargetCellFrame:(NSInteger)targetIndex {
CGFloat x = [self getContentEdgeInsetLeft];
for (int i = 0; i < targetIndex; i ++) {
JXCategoryBaseCellModel *cellModel = self.dataSource[i];
CGFloat cellWidth;
if (cellModel.isTransitionAnimating && cellModel.isCellWidthZoomEnabled) {
//cellWidthCurrentZoomScale
if (cellModel.isSelected) {
cellWidth = [self getCellWidthAtIndex:cellModel.index]*cellModel.cellWidthSelectedZoomScale;
}else {
cellWidth = [self getCellWidthAtIndex:cellModel.index]*cellModel.cellWidthNormalZoomScale;
}
}else {
cellWidth = cellModel.cellWidth;
}
x += cellWidth + self.innerCellSpacing;
}
CGFloat width;
JXCategoryBaseCellModel *selectedCellModel = self.dataSource[targetIndex];
if (selectedCellModel.isTransitionAnimating && selectedCellModel.isCellWidthZoomEnabled) {
width = [self getCellWidthAtIndex:selectedCellModel.index]*selectedCellModel.cellWidthSelectedZoomScale;
}else {
width = selectedCellModel.cellWidth;
}
return CGRectMake(x, 0, width, self.bounds.size.height);
}
- (CGRect)getTargetSelectedCellFrame:(NSInteger)targetIndex selectedType:(JXCategoryCellSelectedType)selectedType {
CGFloat x = [self getContentEdgeInsetLeft];
for (int i = 0; i < targetIndex; i ++) {
JXCategoryBaseCellModel *cellModel = self.dataSource[i];
x += [self getCellWidthAtIndex:cellModel.index] + self.innerCellSpacing;
}
CGFloat cellWidth = 0;
JXCategoryBaseCellModel *selectedCellModel = self.dataSource[targetIndex];
if (selectedCellModel.cellWidthZoomEnabled) {
cellWidth = [self getCellWidthAtIndex:targetIndex]*selectedCellModel.cellWidthSelectedZoomScale;
}else {
cellWidth = [self getCellWidthAtIndex:targetIndex];
}
return CGRectMake(x, 0, cellWidth, self.bounds.size.height);
}
- (void)initializeData {
_firstLayoutSubviews = YES;
_dataSource = [NSMutableArray array];
_selectedIndex = 0;
_cellWidth = JXCategoryViewAutomaticDimension;
_cellWidthIncrement = 0;
_cellSpacing = 20;
_averageCellSpacingEnabled = YES;
_cellWidthZoomEnabled = NO;
_cellWidthZoomScale = 1.2;
_cellWidthZoomScrollGradientEnabled = YES;
_contentEdgeInsetLeft = JXCategoryViewAutomaticDimension;
_contentEdgeInsetRight = JXCategoryViewAutomaticDimension;
_lastContentViewContentOffset = CGPointZero;
_selectedAnimationEnabled = NO;
_selectedAnimationDuration = 0.25;
_scrollingTargetIndex = -1;
_contentScrollViewClickTransitionAnimationEnabled = YES;
_needReloadByBecomeActive = NO;
}
- (void)initializeViews {
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
_collectionView = [[JXCategoryCollectionView alloc] initWithFrame:self.bounds collectionViewLayout:layout];
self.collectionView.backgroundColor = [UIColor clearColor];
self.collectionView.showsHorizontalScrollIndicator = NO;
self.collectionView.showsVerticalScrollIndicator = NO;
self.collectionView.scrollsToTop = NO;
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
[self.collectionView registerClass:[self preferredCellClass] forCellWithReuseIdentifier:NSStringFromClass([self preferredCellClass])];
if (@available(iOS 10.0, *)) {
self.collectionView.prefetchingEnabled = NO;
}
if (@available(iOS 11.0, *)) {
if ([self.collectionView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
if ([RTLManager supportRTL]) {
self.collectionView.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
[RTLManager horizontalFlipView:self.collectionView];
}
[self addSubview:self.collectionView];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void)refreshDataSource {}
- (void)refreshState {
if (self.selectedIndex < 0 || self.selectedIndex >= self.dataSource.count) {
self.defaultSelectedIndex = 0;
}
self.innerCellSpacing = self.cellSpacing;
//+cell+cellSpacing+
__block CGFloat totalItemWidth = [self getContentEdgeInsetLeft];
//cell
CGFloat totalCellWidth = 0;
for (int i = 0; i < self.dataSource.count; i++) {
JXCategoryBaseCellModel *cellModel = self.dataSource[i];
cellModel.index = i;
cellModel.cellWidthZoomEnabled = self.cellWidthZoomEnabled;
cellModel.cellWidthNormalZoomScale = 1;
cellModel.cellWidthSelectedZoomScale = self.cellWidthZoomScale;
cellModel.selectedAnimationEnabled = self.selectedAnimationEnabled;
cellModel.selectedAnimationDuration = self.selectedAnimationDuration;
cellModel.cellSpacing = self.innerCellSpacing;
if (i == self.selectedIndex) {
cellModel.selected = YES;
cellModel.cellWidthCurrentZoomScale = cellModel.cellWidthSelectedZoomScale;
}else {
cellModel.selected = NO;
cellModel.cellWidthCurrentZoomScale = cellModel.cellWidthNormalZoomScale;
}
if (self.isCellWidthZoomEnabled) {
cellModel.cellWidth = [self getCellWidthAtIndex:i]*cellModel.cellWidthCurrentZoomScale;
}else {
cellModel.cellWidth = [self getCellWidthAtIndex:i];
}
totalCellWidth += cellModel.cellWidth;
if (i == self.dataSource.count - 1) {
totalItemWidth += cellModel.cellWidth + [self getContentEdgeInsetRight];
}else {
totalItemWidth += cellModel.cellWidth + self.innerCellSpacing;
}
[self refreshCellModel:cellModel index:i];
}
if (self.isAverageCellSpacingEnabled && totalItemWidth < self.bounds.size.width) {
//cellSpacing
NSInteger cellSpacingItemCount = self.dataSource.count - 1;
CGFloat totalCellSpacingWidth = self.bounds.size.width - totalCellWidth;
//Automatic1
if (self.contentEdgeInsetLeft == JXCategoryViewAutomaticDimension) {
cellSpacingItemCount += 1;
}else {
totalCellSpacingWidth -= self.contentEdgeInsetLeft;
}
//Automatic1
if (self.contentEdgeInsetRight == JXCategoryViewAutomaticDimension) {
cellSpacingItemCount += 1;
}else {
totalCellSpacingWidth -= self.contentEdgeInsetRight;
}
CGFloat cellSpacing = 0;
if (cellSpacingItemCount > 0) {
cellSpacing = totalCellSpacingWidth/cellSpacingItemCount;
}
self.innerCellSpacing = cellSpacing;
[self.dataSource enumerateObjectsUsingBlock:^(JXCategoryBaseCellModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.cellSpacing = self.innerCellSpacing;
}];
}
//---------------------collectionView----------------------
//collectionViewcellindexcontentOffset
__block CGFloat frameXOfSelectedCell = [self getContentEdgeInsetLeft];
__block CGFloat selectedCellWidth = 0;
totalItemWidth = [self getContentEdgeInsetLeft];
[self.dataSource enumerateObjectsUsingBlock:^(JXCategoryBaseCellModel * cellModel, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx < self.selectedIndex) {
frameXOfSelectedCell += cellModel.cellWidth + self.innerCellSpacing;
}else if (idx == self.selectedIndex) {
selectedCellWidth = cellModel.cellWidth;
}
if (idx == self.dataSource.count - 1) {
totalItemWidth += cellModel.cellWidth + [self getContentEdgeInsetRight];
}else {
totalItemWidth += cellModel.cellWidth + self.innerCellSpacing;
}
}];
CGFloat minX = 0;
CGFloat maxX = totalItemWidth - self.bounds.size.width;
CGFloat targetX = frameXOfSelectedCell - self.bounds.size.width/2.0 + selectedCellWidth/2.0;
CGPoint collectionViewContentOffset = self.collectionView.contentOffset;
collectionViewContentOffset.x = MAX(MIN(maxX, targetX), minX);
[self.collectionView setContentOffset:collectionViewContentOffset
animated:NO];
//---------------------collectionView----------------------
if (CGRectEqualToRect(self.contentScrollView.frame, CGRectZero) && self.contentScrollView.superview != nil) {
//JXCategoryViewcontentScrollViewdefaultSelectedIndexcontentScrollViewframezeroframelayoutSubviews
//JXSegmentedListContainerViewcontentScrollView使JXSegmentedListContainerView.superView
UIView *parentView = self.contentScrollView.superview;
while (parentView != nil && CGRectEqualToRect(parentView.frame, CGRectZero)) {
parentView = parentView.superview;
}
[parentView setNeedsLayout];
[parentView layoutIfNeeded];
}
//contentScrollViewcontentOffsetindex
CGPoint contentScrollViewContentOffset = self.contentScrollView.contentOffset;
contentScrollViewContentOffset.x = self.selectedIndex*self.contentScrollView.bounds.size.width;
[self.contentScrollView setContentOffset:contentScrollViewContentOffset animated:NO];
}
- (BOOL)selectCellAtIndex:(NSInteger)targetIndex selectedType:(JXCategoryCellSelectedType)selectedType {
if (targetIndex < 0 || targetIndex >= self.dataSource.count) {
return NO;
}
self.needReloadByBecomeActive = NO;
if (self.selectedIndex == targetIndex) {
//indexindex
if (selectedType == JXCategoryCellSelectedTypeCode) {
[self.listContainer didClickSelectedItemAtIndex:targetIndex];
}else if (selectedType == JXCategoryCellSelectedTypeClick) {
[self.listContainer didClickSelectedItemAtIndex:targetIndex];
if (self.delegateFlags.didClickSelectedItemAtIndexFlag) {
[self.delegate categoryView:self didClickSelectedItemAtIndex:targetIndex];
}
}else if (selectedType == JXCategoryCellSelectedTypeScroll) {
if (self.delegateFlags.didScrollSelectedItemAtIndexFlag) {
[self.delegate categoryView:self didScrollSelectedItemAtIndex:targetIndex];
}
}
if (self.delegateFlags.didSelectedItemAtIndexFlag) {
[self.delegate categoryView:self didSelectedItemAtIndex:targetIndex];
}
self.scrollingTargetIndex = -1;
return NO;
}
//cellModel
JXCategoryBaseCellModel *lastCellModel = self.dataSource[self.selectedIndex];
lastCellModel.selectedType = selectedType;
JXCategoryBaseCellModel *selectedCellModel = self.dataSource[targetIndex];
selectedCellModel.selectedType = selectedType;
[self refreshSelectedCellModel:selectedCellModel unselectedCellModel:lastCellModel];
//cell
JXCategoryBaseCell *lastCell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:self.selectedIndex inSection:0]];
[lastCell reloadData:lastCellModel];
JXCategoryBaseCell *selectedCell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0]];
[selectedCell reloadData:selectedCellModel];
if (self.scrollingTargetIndex != -1 && self.scrollingTargetIndex != targetIndex) {
JXCategoryBaseCellModel *scrollingTargetCellModel = self.dataSource[self.scrollingTargetIndex];
scrollingTargetCellModel.selected = NO;
scrollingTargetCellModel.selectedType = selectedType;
[self refreshSelectedCellModel:selectedCellModel unselectedCellModel:scrollingTargetCellModel];
JXCategoryBaseCell *scrollingTargetCell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:self.scrollingTargetIndex inSection:0]];
[scrollingTargetCell reloadData:scrollingTargetCellModel];
}
if (self.isCellWidthZoomEnabled) {
[self.collectionView.collectionViewLayout invalidateLayout];
//cellwidthcellscrollToItembucellWidthindexcell
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.selectedAnimationDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
});
} else {
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
if (selectedType == JXCategoryCellSelectedTypeClick ||
selectedType == JXCategoryCellSelectedTypeCode) {
CGPoint offset = self.contentScrollView.contentOffset;
offset.x =
targetIndex*self.contentScrollView.bounds.size.width;
[self.contentScrollView setContentOffset:offset
animated:self.isContentScrollViewClickTransitionAnimationEnabled];
}
self.selectedIndex = targetIndex;
if (selectedType == JXCategoryCellSelectedTypeCode) {
[self.listContainer didClickSelectedItemAtIndex:targetIndex];
} else if (selectedType == JXCategoryCellSelectedTypeClick) {
[self.listContainer didClickSelectedItemAtIndex:targetIndex];
if (self.delegateFlags.didClickSelectedItemAtIndexFlag) {
[self.delegate categoryView:self didClickSelectedItemAtIndex:targetIndex];
}
} else if(selectedType == JXCategoryCellSelectedTypeScroll) {
if (self.delegateFlags.didScrollSelectedItemAtIndexFlag) {
[self.delegate categoryView:self didScrollSelectedItemAtIndex:targetIndex];
}
}
if (self.delegateFlags.didSelectedItemAtIndexFlag) {
[self.delegate categoryView:self didSelectedItemAtIndex:targetIndex];
}
self.scrollingTargetIndex = -1;
return YES;
}
- (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel {
selectedCellModel.selected = YES;
unselectedCellModel.selected = NO;
if (self.isCellWidthZoomEnabled) {
if (selectedCellModel.selectedType == JXCategoryCellSelectedTypeCode ||
selectedCellModel.selectedType == JXCategoryCellSelectedTypeClick) {
self.animator = [[JXCategoryViewAnimator alloc] init];
self.animator.duration = self.selectedAnimationDuration;
__weak typeof(self) weakSelf = self;
self.animator.progressCallback = ^(CGFloat percent) {
selectedCellModel.transitionAnimating = YES;
unselectedCellModel.transitionAnimating = YES;
selectedCellModel.cellWidthCurrentZoomScale = [JXCategoryFactory interpolationFrom:selectedCellModel.cellWidthNormalZoomScale to:selectedCellModel.cellWidthSelectedZoomScale percent:percent];
selectedCellModel.cellWidth = [weakSelf getCellWidthAtIndex:selectedCellModel.index] * selectedCellModel.cellWidthCurrentZoomScale;
unselectedCellModel.cellWidthCurrentZoomScale = [JXCategoryFactory interpolationFrom:unselectedCellModel.cellWidthSelectedZoomScale to:unselectedCellModel.cellWidthNormalZoomScale percent:percent];
unselectedCellModel.cellWidth = [weakSelf getCellWidthAtIndex:unselectedCellModel.index] * unselectedCellModel.cellWidthCurrentZoomScale;
[weakSelf.collectionView.collectionViewLayout invalidateLayout];
};
self.animator.completeCallback = ^{
selectedCellModel.transitionAnimating = NO;
unselectedCellModel.transitionAnimating = NO;
};
[self.animator start];
} else {
selectedCellModel.cellWidthCurrentZoomScale = selectedCellModel.cellWidthSelectedZoomScale;
selectedCellModel.cellWidth = [self getCellWidthAtIndex:selectedCellModel.index] * selectedCellModel.cellWidthCurrentZoomScale;
unselectedCellModel.cellWidthCurrentZoomScale = unselectedCellModel.cellWidthNormalZoomScale;
unselectedCellModel.cellWidth = [self getCellWidthAtIndex:unselectedCellModel.index] * unselectedCellModel.cellWidthCurrentZoomScale;
}
}
}
- (void)contentOffsetOfContentScrollViewDidChanged:(CGPoint)contentOffset {
if (self.dataSource.count == 0) {
return;
}
CGFloat ratio = contentOffset.x/self.contentScrollView.bounds.size.width;
if (ratio > self.dataSource.count - 1 || ratio < 0) {
//
return;
}
if (contentOffset.x == 0 && self.selectedIndex == 0 && self.lastContentViewContentOffset.x == 0) {
//contentOffset.x0
return;
}
CGFloat maxContentOffsetX = self.contentScrollView.contentSize.width - self.contentScrollView.bounds.size.width;
if (contentOffset.x == maxContentOffsetX && self.selectedIndex == self.dataSource.count - 1 && self.lastContentViewContentOffset.x == maxContentOffsetX) {
//contentOffset.xmaxContentOffsetX
return;
}
ratio = MAX(0, MIN(self.dataSource.count - 1, ratio));
NSInteger baseIndex = floorf(ratio);
CGFloat remainderRatio = ratio - baseIndex;
if (remainderRatio == 0) {
//contentScrollView
//contentOffsetindex1contentOffsetCGPoint(width, 0)
if (!(self.lastContentViewContentOffset.x == contentOffset.x && self.selectedIndex == baseIndex)) {
[self scrollSelectItemAtIndex:baseIndex];
}
} else {
self.needReloadByBecomeActive = YES;
if (self.animator.isExecuting) {
[self.animator invalid];
//animator.progessCallback
for (JXCategoryBaseCellModel *model in self.dataSource) {
if (model.isSelected) {
model.cellWidthCurrentZoomScale = model.cellWidthSelectedZoomScale;
model.cellWidth = [self getCellWidthAtIndex:model.index] * model.cellWidthCurrentZoomScale;
}else {
model.cellWidthCurrentZoomScale = model.cellWidthNormalZoomScale;
model.cellWidth = [self getCellWidthAtIndex:model.index] * model.cellWidthCurrentZoomScale;
}
}
}
//remainderRatio0
if (fabs(ratio - self.selectedIndex) > 1) {
NSInteger targetIndex = baseIndex;
if (ratio < self.selectedIndex) {
targetIndex = baseIndex + 1;
}
[self scrollSelectItemAtIndex:targetIndex];
}
if (self.selectedIndex == baseIndex) {
self.scrollingTargetIndex = baseIndex + 1;
} else {
self.scrollingTargetIndex = baseIndex;
}
if (self.isCellWidthZoomEnabled && self.isCellWidthZoomScrollGradientEnabled) {
JXCategoryBaseCellModel *leftCellModel = (JXCategoryBaseCellModel *)self.dataSource[baseIndex];
JXCategoryBaseCellModel *rightCellModel = (JXCategoryBaseCellModel *)self.dataSource[baseIndex + 1];
leftCellModel.cellWidthCurrentZoomScale = [JXCategoryFactory interpolationFrom:leftCellModel.cellWidthSelectedZoomScale to:leftCellModel.cellWidthNormalZoomScale percent:remainderRatio];
leftCellModel.cellWidth = [self getCellWidthAtIndex:leftCellModel.index] * leftCellModel.cellWidthCurrentZoomScale;
rightCellModel.cellWidthCurrentZoomScale = [JXCategoryFactory interpolationFrom:rightCellModel.cellWidthNormalZoomScale to:rightCellModel.cellWidthSelectedZoomScale percent:remainderRatio];
rightCellModel.cellWidth = [self getCellWidthAtIndex:rightCellModel.index] * rightCellModel.cellWidthCurrentZoomScale;
[self.collectionView.collectionViewLayout invalidateLayout];
}
if (self.delegateFlags.scrollingFromLeftIndexToRightIndexFlag) {
[self.delegate categoryView:self scrollingFromLeftIndex:baseIndex toRightIndex:baseIndex + 1 ratio:remainderRatio];
}
}
}
- (CGFloat)preferredCellWidthAtIndex:(NSInteger)index {
return 0;
}
- (Class)preferredCellClass {
return JXCategoryBaseCell.class;
}
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index {
}
@end