This commit is contained in:
2025-11-06 19:19:12 +08:00
parent a75afbe4c1
commit 6ba1339c0b
195 changed files with 13443 additions and 2729 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

View File

@@ -0,0 +1,24 @@
//
// JXCategoryCollectionView.h
// UI系列测试
//
// Created by jiaxin on 2018/3/21.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "JXCategoryIndicatorProtocol.h"
@class JXCategoryCollectionView;
@protocol JXCategoryCollectionViewGestureDelegate <NSObject>
@optional
- (BOOL)categoryCollectionView:(JXCategoryCollectionView *)collectionView gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
- (BOOL)categoryCollectionView:(JXCategoryCollectionView *)collectionView gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
@end
@interface JXCategoryCollectionView : UICollectionView
@property (nonatomic, strong) NSArray <UIView<JXCategoryIndicatorProtocol> *> *indicators;
@property (nonatomic, weak) id<JXCategoryCollectionViewGestureDelegate> gestureDelegate;
@end

View File

@@ -0,0 +1,52 @@
//
// JXCategoryCollectionView.m
// UI
//
// Created by jiaxin on 2018/3/21.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryCollectionView.h"
@interface JXCategoryCollectionView ()<UIGestureRecognizerDelegate>
@end
@implementation JXCategoryCollectionView
- (void)setIndicators:(NSArray<UIView<JXCategoryIndicatorProtocol> *> *)indicators {
for (UIView *indicator in _indicators) {
//indicator
[indicator removeFromSuperview];
}
_indicators = indicators;
for (UIView *indicator in indicators) {
[self addSubview:indicator];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
for (UIView<JXCategoryIndicatorProtocol> *view in self.indicators) {
[self sendSubviewToBack:view];
}
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if (self.gestureDelegate && [self.gestureDelegate respondsToSelector:@selector(categoryCollectionView:gestureRecognizerShouldBegin:)]) {
return [self.gestureDelegate categoryCollectionView:self gestureRecognizerShouldBegin:gestureRecognizer];
}
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if (self.gestureDelegate && [self.gestureDelegate respondsToSelector:@selector(categoryCollectionView:gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]) {
return [self.gestureDelegate categoryCollectionView:self gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
}
return NO;
}
@end

View File

@@ -0,0 +1,18 @@
//
// JXCategoryFactory.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface JXCategoryFactory : NSObject
+ (CGFloat)interpolationFrom:(CGFloat)from to:(CGFloat)to percent:(CGFloat)percent;
+ (UIColor *)interpolationColorFrom:(UIColor *)fromColor to:(UIColor *)toColor percent:(CGFloat)percent;
@end

View File

@@ -0,0 +1,29 @@
//
// JXCategoryFactory.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryFactory.h"
#import "UIColor+JXAdd.h"
@implementation JXCategoryFactory
+ (CGFloat)interpolationFrom:(CGFloat)from to:(CGFloat)to percent:(CGFloat)percent
{
percent = MAX(0, MIN(1, percent));
return from + (to - from)*percent;
}
+ (UIColor *)interpolationColorFrom:(UIColor *)fromColor to:(UIColor *)toColor percent:(CGFloat)percent
{
CGFloat red = [self interpolationFrom:fromColor.jx_red to:toColor.jx_red percent:percent];
CGFloat green = [self interpolationFrom:fromColor.jx_green to:toColor.jx_green percent:percent];
CGFloat blue = [self interpolationFrom:fromColor.jx_blue to:toColor.jx_blue percent:percent];
CGFloat alpha = [self interpolationFrom:fromColor.jx_alpha to:toColor.jx_alpha percent:percent];
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
}
@end

View File

@@ -0,0 +1,31 @@
//
// JXCategoryIndicatorParamsModel.h
// JXCategoryView
//
// Created by jiaxin on 2018/12/13.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "JXCategoryViewDefines.h"
/**
指示器不同情况处理时传递的数据模型,不同情况会对不同的属性赋值,根据不同情况的 api 说明确认。
FAQ: 为什么会通过 model 传递数据?
因为指示器处理逻辑以后会扩展不同的使用场景,会新增参数,如果不通过 model 传递,就会在 api 新增参数,一旦修改 api 改的地方就特别多了,而且会影响到之前自定义实现的开发者。
*/
@interface JXCategoryIndicatorParamsModel : NSObject
@property (nonatomic, assign) NSInteger selectedIndex; // 当前选中的 index
@property (nonatomic, assign) CGRect selectedCellFrame; // 当前选中的 cellFrame
@property (nonatomic, assign) NSInteger leftIndex; // 正在过渡中的两个 cell相对位置在左边的 cell 的 index
@property (nonatomic, assign) CGRect leftCellFrame; // 正在过渡中的两个 cell相对位置在左边的 cell 的 frame
@property (nonatomic, assign) NSInteger rightIndex; // 正在过渡中的两个 cell相对位置在右边的 cell 的 index
@property (nonatomic, assign) CGRect rightCellFrame; // 正在过渡中的两个 cell相对位置在右边的 cell 的 frame
@property (nonatomic, assign) CGFloat percent; // 正在过渡中的两个 cell从左到右的百分比
@property (nonatomic, assign) NSInteger lastSelectedIndex; // 之前选中的 index
@property (nonatomic, assign) JXCategoryCellSelectedType selectedType; //cell 被选中类型
@end

View File

@@ -0,0 +1,13 @@
//
// JXCategoryIndicatorParamsModel.m
// JXCategoryView
//
// Created by jiaxin on 2018/12/13.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorParamsModel.h"
@implementation JXCategoryIndicatorParamsModel
@end

View File

@@ -0,0 +1,49 @@
//
// JXCategoryIndicatorProtocol.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "JXCategoryViewDefines.h"
#import "JXCategoryIndicatorParamsModel.h"
@protocol JXCategoryIndicatorProtocol <NSObject>
/**
categoryView 重置状态时调用
param selectedIndex 当前选中的 index
param selectedCellFrame 当前选中的 cellFrame
@param model 数据模型
*/
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model;
/**
contentScrollView在进行手势滑动时处理指示器跟随手势变化UI逻辑
param selectedIndex 当前选中的index
param leftIndex 正在过渡中的两个cell相对位置在左边的cell的index
param leftCellFrame 正在过渡中的两个cell相对位置在左边的cell的frame
param rightIndex 正在过渡中的两个cell相对位置在右边的cell的index
param rightCellFrame 正在过渡中的两个cell相对位置在右边的cell的frame
param percent 过渡百分比
@param model 数据模型
*/
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model;
/**
选中了某一个cell
param lastSelectedIndex 之前选中的index
param selectedIndex 选中的index
param selectedCellFrame 选中的cellFrame
param selectedType cell被选中类型
@param model 数据模型
*/
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model;
@end

View File

@@ -0,0 +1,16 @@
//
// JXCategoryListContainerRTLCell.h
// JXCategoryView
//
// Created by jiaxin on 2020/7/3.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface JXCategoryListContainerRTLCell : UICollectionViewCell
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,23 @@
//
// JXCategoryListContainerRTLCell.m
// JXCategoryView
//
// Created by jiaxin on 2020/7/3.
//
#import "JXCategoryListContainerRTLCell.h"
#import "RTLManager.h"
@implementation JXCategoryListContainerRTLCell
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[RTLManager horizontalFlipViewIfNeeded:self];
[RTLManager horizontalFlipViewIfNeeded:self.contentView];
}
return self;
}
@end

View File

@@ -0,0 +1,122 @@
//
// JXCategoryListScrollView.h
// JXCategoryView
//
// Created by jiaxin on 2018/9/12.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "JXCategoryViewDefines.h"
#import "JXCategoryBaseView.h"
@class JXCategoryListContainerView;
/**
列表容器视图的类型
- ScrollView: UIScrollView。优势没有其他副作用。劣势视图内存占用相对大一点。
- CollectionView: 使用UICollectionView。优势因为列表被添加到cell上视图的内存占用更少适合内存要求特别高的场景。劣势因为cell重用机制的问题导致列表下拉刷新视图会因为被removeFromSuperview而被隐藏。需要参考`LoadDataListCollectionListViewController`类做特殊处理。
*/
typedef NS_ENUM(NSUInteger, JXCategoryListContainerType) {
JXCategoryListContainerType_ScrollView,
JXCategoryListContainerType_CollectionView,
};
@protocol JXCategoryListContentViewDelegate <NSObject>
/**
如果列表是VC就返回VC.view
如果列表是View就返回View自己
@return 返回列表视图
*/
- (UIView *)listView;
@optional
/**
可选实现,列表将要显示的时候调用
*/
- (void)listWillAppear;
/**
可选实现,列表显示的时候调用
*/
- (void)listDidAppear;
/**
可选实现,列表将要消失的时候调用
*/
- (void)listWillDisappear;
/**
可选实现,列表消失的时候调用
*/
- (void)listDidDisappear;
@end
@protocol JXCategoryListContainerViewDelegate <NSObject>
/**
返回list的数量
@param listContainerView 列表的容器视图
@return list的数量
*/
- (NSInteger)numberOfListsInlistContainerView:(JXCategoryListContainerView *)listContainerView;
/**
根据index返回一个对应列表实例需要是遵从`JXCategoryListContentViewDelegate`协议的对象。
你可以代理方法调用的时候初始化对应列表,达到懒加载的效果。这也是默认推荐的初始化列表方法。你也可以提前创建好列表,等该代理方法回调的时候再返回也可以,达到预加载的效果。
如果列表是用自定义UIView封装的就让自定义UIView遵从`JXCategoryListContentViewDelegate`协议该方法返回自定义UIView即可。
如果列表是用自定义UIViewController封装的就让自定义UIViewController遵从`JXCategoryListContentViewDelegate`协议该方法返回自定义UIViewController即可。
@param listContainerView 列表的容器视图
@param index 目标下标
@return 遵从JXCategoryListContentViewDelegate协议的list实例
*/
- (id<JXCategoryListContentViewDelegate>)listContainerView:(JXCategoryListContainerView *)listContainerView initListForIndex:(NSInteger)index;
@optional
/**
返回自定义UIScrollView或UICollectionView的Class
某些特殊情况需要自己处理UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture需要处理手势相关代理。
@param listContainerView JXCategoryListContainerView
@return 自定义UIScrollView实例
*/
- (Class)scrollViewClassInlistContainerView:(JXCategoryListContainerView *)listContainerView;
/**
控制能否初始化对应index的列表。有些业务需求需要在某些情况才允许初始化某些列表通过通过该代理实现控制。
*/
- (BOOL)listContainerView:(JXCategoryListContainerView *)listContainerView canInitListAtIndex:(NSInteger)index;
- (void)listContainerViewDidScroll:(UIScrollView *)scrollView;
- (void)listContainerViewWillBeginDragging:(UIScrollView *)scrollView;
- (void)listContainerViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)listContainerViewWillBeginDecelerating:(UIScrollView *)scrollView;
- (void)listContainerViewDidEndDecelerating:(UIScrollView *)scrollView;
@end
@interface JXCategoryListContainerView : UIView <JXCategoryViewListContainer>
@property (nonatomic, assign, readonly) JXCategoryListContainerType containerType;
@property (nonatomic, strong, readonly) UIScrollView *scrollView;
@property (nonatomic, strong, readonly) NSDictionary <NSNumber *, id<JXCategoryListContentViewDelegate>> *validListDict; //已经加载过的列表字典。key是indexvalue是对应的列表
@property (nonatomic, strong) UIColor *listCellBackgroundColor; //默认:[UIColor whiteColor]
/**
滚动切换的时候滚动距离超过一页的多少百分比就触发列表的初始化。默认0.01即列表显示了一点就触发加载。范围0~1开区间不包括0和1
*/
@property (nonatomic, assign) CGFloat initListPercent;
@property (nonatomic, assign) BOOL bounces; //默认NO
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
- (instancetype)initWithType:(JXCategoryListContainerType)type delegate:(id<JXCategoryListContainerViewDelegate>)delegate NS_DESIGNATED_INITIALIZER;
@end

View File

@@ -0,0 +1,547 @@
//
// JXCategoryListContainerView.m
// JXCategoryView
//
// Created by jiaxin on 2018/9/12.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryListContainerView.h"
#import <objc/runtime.h>
#import "RTLManager.h"
@interface JXCategoryListContainerViewController : UIViewController
@property (copy) void(^viewWillAppearBlock)(void);
@property (copy) void(^viewDidAppearBlock)(void);
@property (copy) void(^viewWillDisappearBlock)(void);
@property (copy) void(^viewDidDisappearBlock)(void);
@end
@implementation JXCategoryListContainerViewController
- (void)dealloc
{
self.viewWillAppearBlock = nil;
self.viewDidAppearBlock = nil;
self.viewWillDisappearBlock = nil;
self.viewDidDisappearBlock = nil;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.viewWillAppearBlock();
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.viewDidAppearBlock();
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
self.viewWillDisappearBlock();
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
self.viewDidDisappearBlock();
}
- (BOOL)shouldAutomaticallyForwardAppearanceMethods { return NO; }
@end
@interface JXCategoryListContainerView () <UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, weak) id<JXCategoryListContainerViewDelegate> delegate;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic, strong) NSMutableDictionary <NSNumber *, id<JXCategoryListContentViewDelegate>> *validListDict;
@property (nonatomic, assign) NSInteger willAppearIndex;
@property (nonatomic, assign) NSInteger willDisappearIndex;
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) JXCategoryListContainerViewController *containerVC;
@end
@implementation JXCategoryListContainerView
- (instancetype)initWithType:(JXCategoryListContainerType)type delegate:(id<JXCategoryListContainerViewDelegate>)delegate{
self = [super initWithFrame:CGRectZero];
if (self) {
_containerType = type;
_delegate = delegate;
_validListDict = [NSMutableDictionary dictionary];
_willAppearIndex = -1;
_willDisappearIndex = -1;
_initListPercent = 0.01;
[self initializeViews];
}
return self;
}
- (void)initializeViews {
_listCellBackgroundColor = [UIColor whiteColor];
_containerVC = [[JXCategoryListContainerViewController alloc] init];
self.containerVC.view.backgroundColor = [UIColor clearColor];
[self addSubview:self.containerVC.view];
__weak typeof(self) weakSelf = self;
self.containerVC.viewWillAppearBlock = ^{
[weakSelf listWillAppear:weakSelf.currentIndex];
};
self.containerVC.viewDidAppearBlock = ^{
[weakSelf listDidAppear:weakSelf.currentIndex];
};
self.containerVC.viewWillDisappearBlock = ^{
[weakSelf listWillDisappear:weakSelf.currentIndex];
};
self.containerVC.viewDidDisappearBlock = ^{
[weakSelf listDidDisappear:weakSelf.currentIndex];
};
if (self.containerType == JXCategoryListContainerType_ScrollView) {
if (self.delegate &&
[self.delegate respondsToSelector:@selector(scrollViewClassInlistContainerView:)] &&
[[self.delegate scrollViewClassInlistContainerView:self] isKindOfClass:object_getClass([UIScrollView class])]) {
_scrollView = (UIScrollView *)[[[self.delegate scrollViewClassInlistContainerView:self] alloc] init];
}else {
_scrollView = [[UIScrollView alloc] init];
}
self.scrollView.delegate = self;
self.scrollView.pagingEnabled = YES;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.showsVerticalScrollIndicator = NO;
self.scrollView.scrollsToTop = NO;
self.scrollView.bounces = NO;
if (@available(iOS 11.0, *)) {
if ([self.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
[RTLManager horizontalFlipViewIfNeeded:self.scrollView];
[self.containerVC.view addSubview:self.scrollView];
}else {
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.minimumLineSpacing = 0;
layout.minimumInteritemSpacing = 0;
if (self.delegate &&
[self.delegate respondsToSelector:@selector(scrollViewClassInlistContainerView:)] &&
[[self.delegate scrollViewClassInlistContainerView:self] isKindOfClass:object_getClass([UICollectionView class])]) {
_collectionView = (UICollectionView *)[[[self.delegate scrollViewClassInlistContainerView:self] alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
}else {
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
}
self.collectionView.pagingEnabled = YES;
self.collectionView.showsHorizontalScrollIndicator = NO;
self.collectionView.showsVerticalScrollIndicator = NO;
self.collectionView.scrollsToTop = NO;
self.collectionView.bounces = NO;
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cell"];
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.containerVC.view addSubview:self.collectionView];
//访scrollView
_scrollView = _collectionView;
}
}
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
UIResponder *next = newSuperview;
while (next != nil) {
if ([next isKindOfClass:[UIViewController class]]) {
[((UIViewController *)next) addChildViewController:self.containerVC];
break;
}
next = next.nextResponder;
}
}
- (void)layoutSubviews {
[super layoutSubviews];
self.containerVC.view.frame = self.bounds;
if (self.containerType == JXCategoryListContainerType_ScrollView) {
if (CGRectEqualToRect(self.scrollView.frame, CGRectZero) || !CGSizeEqualToSize(self.scrollView.bounds.size, self.bounds.size)) {
self.scrollView.frame = self.bounds;
self.scrollView.contentSize = CGSizeMake(self.scrollView.bounds.size.width*[self.delegate numberOfListsInlistContainerView:self], self.scrollView.bounds.size.height);
[_validListDict enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull index, id<JXCategoryListContentViewDelegate> _Nonnull list, BOOL * _Nonnull stop) {
[list listView].frame = CGRectMake(index.intValue*self.scrollView.bounds.size.width, 0, self.scrollView.bounds.size.width, self.scrollView.bounds.size.height);
}];
CGPoint scrollViewContentOffset = self.scrollView.contentOffset;
scrollViewContentOffset.x = self.currentIndex*self.scrollView.bounds.size.width;
self.scrollView.contentOffset = scrollViewContentOffset;
}else {
self.scrollView.frame = self.bounds;
self.scrollView.contentSize = CGSizeMake(self.scrollView.bounds.size.width*[self.delegate numberOfListsInlistContainerView:self], self.scrollView.bounds.size.height);
}
}else {
if (CGRectEqualToRect(self.collectionView.frame, CGRectZero) || !CGSizeEqualToSize(self.collectionView.bounds.size, self.bounds.size)) {
[self.collectionView.collectionViewLayout invalidateLayout];
self.collectionView.frame = self.bounds;
[self.collectionView reloadData];
CGPoint collectionViewContentOffset = self.collectionView.contentOffset;
collectionViewContentOffset.x =
self.collectionView.bounds.size.width*self.currentIndex;
[self.collectionView setContentOffset:collectionViewContentOffset animated:NO];
}else {
self.collectionView.frame = self.bounds;
}
}
}
- (void)setinitListPercent:(CGFloat)initListPercent {
_initListPercent = initListPercent;
if (initListPercent <= 0 || initListPercent >= 1) {
NSAssert(NO, @"initListPercent值范围为开区间(0,1)即不包括0和1");
}
}
- (void)setBounces:(BOOL)bounces {
_bounces = bounces;
self.scrollView.bounces = bounces;
}
#pragma mark - UICollectionViewDelegate, UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return [self.delegate numberOfListsInlistContainerView:self];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
cell.contentView.backgroundColor = self.listCellBackgroundColor;
UIView* listView = nil;
id<JXCategoryListContentViewDelegate> list = _validListDict[@(indexPath.item)];
if (list != nil) {
//fixme:listUIViewControllerframe`[list listView].frame = cell.bounds;`list vc:
//- (void)loadView {
// self.view = [[UIView alloc] init];
//}
//UIViewControllerview使bug
listView = [list listView];
if ([list isKindOfClass:[UIViewController class]]) {
listView.frame = cell.contentView.bounds;
} else {
listView.frame = cell.bounds;
}
}
BOOL isAdded = NO;
for (UIView *subview in cell.contentView.subviews) {
if( listView != subview ) {
[subview removeFromSuperview];
} else {
isAdded = YES;
}
}
if( !isAdded && listView ) {
[cell.contentView addSubview:listView];
}
// RTL
if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute]
== UIUserInterfaceLayoutDirectionRightToLeft) {
cell.contentView.transform = CGAffineTransformMakeScale(-1, 1);
}
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return self.bounds.size;
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerViewDidScroll:)]) {
[self.delegate listContainerViewDidScroll:scrollView];
}
if (!scrollView.isDragging && !scrollView.isTracking && !scrollView.isDecelerating) {
return;
}
CGFloat ratio = scrollView.contentOffset.x/scrollView.bounds.size.width;
NSInteger maxCount = round(scrollView.contentSize.width/scrollView.bounds.size.width);
NSInteger leftIndex = floorf(ratio);
leftIndex = MAX(0, MIN(maxCount - 1, leftIndex));
NSInteger rightIndex = leftIndex + 1;
if (ratio < 0 || rightIndex >= maxCount) {
[self listDidAppearOrDisappear:scrollView];
return;
}
CGFloat remainderRatio = ratio - leftIndex;
if (rightIndex == self.currentIndex) {
//
if (self.validListDict[@(leftIndex)] == nil && remainderRatio < (1 - self.initListPercent)) {
[self initListIfNeededAtIndex:leftIndex];
}else if (self.validListDict[@(leftIndex)] != nil) {
if (self.willAppearIndex == -1) {
self.willAppearIndex = leftIndex;
[self listWillAppear:self.willAppearIndex];
}
}
if (self.willDisappearIndex == -1) {
self.willDisappearIndex = rightIndex;
[self listWillDisappear:self.willDisappearIndex];
}
}else {
//
if (self.validListDict[@(rightIndex)] == nil && remainderRatio > self.initListPercent) {
[self initListIfNeededAtIndex:rightIndex];
}else if (self.validListDict[@(rightIndex)] != nil) {
if (self.willAppearIndex == -1) {
self.willAppearIndex = rightIndex;
[self listWillAppear:self.willAppearIndex];
}
}
if (self.willDisappearIndex == -1) {
self.willDisappearIndex = leftIndex;
[self listWillDisappear:self.willDisappearIndex];
}
}
[self listDidAppearOrDisappear:scrollView];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
//
if (self.willDisappearIndex != -1) {
[self listWillAppear:self.willDisappearIndex];
[self listWillDisappear:self.willAppearIndex];
[self listDidAppear:self.willDisappearIndex];
[self listDidDisappear:self.willAppearIndex];
self.willDisappearIndex = -1;
self.willAppearIndex = -1;
}
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerViewDidEndDecelerating:)]) {
[self.delegate listContainerViewDidEndDecelerating:scrollView];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerViewWillBeginDragging:)]) {
[self.delegate listContainerViewWillBeginDragging:scrollView];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerViewDidEndDragging:willDecelerate:)]) {
[self.delegate listContainerViewDidEndDragging:scrollView willDecelerate:decelerate];
}
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerViewWillBeginDecelerating:)]) {
[self.delegate listContainerViewWillBeginDecelerating:scrollView];
}
}
#pragma mark - JXCategoryViewListContainer
- (UIScrollView *)contentScrollView {
return self.scrollView;
}
- (void)setDefaultSelectedIndex:(NSInteger)index {
self.currentIndex = index;
}
- (void)didClickSelectedItemAtIndex:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
self.willAppearIndex = -1;
self.willDisappearIndex = -1;
if (self.currentIndex != index) {
[self listWillDisappear:self.currentIndex];
[self listDidDisappear:self.currentIndex];
[self listWillAppear:index];
[self listDidAppear:index];
}
}
- (void)reloadData {
for (id<JXCategoryListContentViewDelegate> list in _validListDict.allValues) {
[[list listView] removeFromSuperview];
if ([list isKindOfClass:[UIViewController class]]) {
[(UIViewController *)list removeFromParentViewController];
}
}
[_validListDict removeAllObjects];
if (self.containerType == JXCategoryListContainerType_ScrollView) {
self.scrollView.contentSize = CGSizeMake(self.scrollView.bounds.size.width*[self.delegate numberOfListsInlistContainerView:self], self.scrollView.bounds.size.height);
}else {
[self.collectionView reloadData];
}
[self listWillAppear:self.currentIndex];
[self listDidAppear:self.currentIndex];
}
#pragma mark - Private
- (void)initListIfNeededAtIndex:(NSInteger)index {
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerView:canInitListAtIndex:)]) {
BOOL canInitList = [self.delegate listContainerView:self canInitListAtIndex:index];
if (!canInitList) {
return;
}
}
id<JXCategoryListContentViewDelegate> list = _validListDict[@(index)];
if (list != nil) {
//
return;
}
list = [self.delegate listContainerView:self initListForIndex:index];
if ([list isKindOfClass:[UIViewController class]]) {
[self.containerVC addChildViewController:(UIViewController *)list];
}
_validListDict[@(index)] = list;
if (self.containerType == JXCategoryListContainerType_ScrollView) {
[list listView].frame = CGRectMake(index*self.scrollView.bounds.size.width, 0, self.scrollView.bounds.size.width, self.scrollView.bounds.size.height);
[self.scrollView addSubview:[list listView]];
[RTLManager horizontalFlipViewIfNeeded:[list listView]];
}else {
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
for (UIView *subview in cell.contentView.subviews) {
[subview removeFromSuperview];
}
[list listView].frame = cell.contentView.bounds;
[cell.contentView addSubview:[list listView]];
}
}
- (void)listWillAppear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
id<JXCategoryListContentViewDelegate> list = _validListDict[@(index)];
if (list == nil) {
//listWillAppear
BOOL canInitList = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerView:canInitListAtIndex:)]) {
canInitList = [self.delegate listContainerView:self canInitListAtIndex:index];
}
if (!canInitList) {
return;
}
list = [self.delegate listContainerView:self initListForIndex:index];
if ([list isKindOfClass:[UIViewController class]]) {
[self.containerVC addChildViewController:(UIViewController *)list];
}
_validListDict[@(index)] = list;
if (self.containerType == JXCategoryListContainerType_ScrollView) {
if ([list listView].superview == nil) {
[list listView].frame = CGRectMake(index*self.scrollView.bounds.size.width, 0, self.scrollView.bounds.size.width, self.scrollView.bounds.size.height);
[self.scrollView addSubview:[list listView]];
[RTLManager horizontalFlipViewIfNeeded:[list listView]];
}
}else {
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
for (UIView *subview in cell.contentView.subviews) {
[subview removeFromSuperview];
}
[list listView].frame = cell.contentView.bounds;
[cell.contentView addSubview:[list listView]];
}
}
if (list && [list respondsToSelector:@selector(listWillAppear)]) {
[list listWillAppear];
}
if ([list isKindOfClass:[UIViewController class]]) {
UIViewController *listVC = (UIViewController *)list;
[listVC beginAppearanceTransition:YES animated:NO];
}
}
- (void)listDidAppear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
self.currentIndex = index;
id<JXCategoryListContentViewDelegate> list = _validListDict[@(index)];
if (list && [list respondsToSelector:@selector(listDidAppear)]) {
[list listDidAppear];
}
if ([list isKindOfClass:[UIViewController class]]) {
UIViewController *listVC = (UIViewController *)list;
[listVC endAppearanceTransition];
}
}
- (void)listWillDisappear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
id<JXCategoryListContentViewDelegate> list = _validListDict[@(index)];
if (list && [list respondsToSelector:@selector(listWillDisappear)]) {
[list listWillDisappear];
}
if ([list isKindOfClass:[UIViewController class]]) {
UIViewController *listVC = (UIViewController *)list;
[listVC beginAppearanceTransition:NO animated:NO];
}
}
- (void)listDidDisappear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
id<JXCategoryListContentViewDelegate> list = _validListDict[@(index)];
if (list && [list respondsToSelector:@selector(listDidDisappear)]) {
[list listDidDisappear];
}
if ([list isKindOfClass:[UIViewController class]]) {
UIViewController *listVC = (UIViewController *)list;
[listVC endAppearanceTransition];
}
}
- (BOOL)checkIndexValid:(NSInteger)index {
NSUInteger count = [self.delegate numberOfListsInlistContainerView:self];
if (count <= 0 || index >= count) {
return NO;
}
return YES;
}
- (void)listDidAppearOrDisappear:(UIScrollView *)scrollView {
CGFloat currentIndexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width;
if (self.willAppearIndex != -1 || self.willDisappearIndex != -1) {
NSInteger disappearIndex = self.willDisappearIndex;
NSInteger appearIndex = self.willAppearIndex;
if (self.willAppearIndex > self.willDisappearIndex) {
//
if (currentIndexPercent >= self.willAppearIndex) {
self.willDisappearIndex = -1;
self.willAppearIndex = -1;
[self listDidDisappear:disappearIndex];
[self listDidAppear:appearIndex];
}
}else {
//
if (currentIndexPercent <= self.willAppearIndex) {
self.willDisappearIndex = -1;
self.willAppearIndex = -1;
[self listDidDisappear:disappearIndex];
[self listDidAppear:appearIndex];
}
}
}
}
@end

View File

@@ -0,0 +1,24 @@
//
// JXCategoryViewAnimator.h
// JXCategoryView
//
// Created by jiaxin on 2019/1/24.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface JXCategoryViewAnimator : NSObject
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, copy) void(^progressCallback)(CGFloat percent);
@property (nonatomic, copy) void(^completeCallback)(void);
@property (readonly, getter=isExecuting) BOOL executing;
- (void)start;
- (void)stop;
- (void)invalid;
@end

View File

@@ -0,0 +1,75 @@
//
// JXCategoryViewAnimator.m
// JXCategoryView
//
// Created by jiaxin on 2019/1/24.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import "JXCategoryViewAnimator.h"
@interface JXCategoryViewAnimator ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, assign) CFTimeInterval firstTimestamp;
@property (readwrite, getter=isExecuting) BOOL executing;
@end
@implementation JXCategoryViewAnimator
#pragma mark - Initialize
- (void)dealloc {
self.progressCallback = nil;
self.completeCallback = nil;
}
- (instancetype)init {
self = [super init];
if (self) {
_executing = NO;
_duration = 0.25;
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(processDisplayLink:)];
}
return self;
}
#pragma mark - Public
- (void)start {
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
self.executing = YES;
}
- (void)stop {
!self.progressCallback ?: self.progressCallback(1);
[self.displayLink invalidate];
!self.completeCallback ?: self.completeCallback();
self.executing = NO;
}
- (void)invalid {
[self.displayLink invalidate];
!self.completeCallback ?: self.completeCallback();
self.executing = NO;
}
#pragma mark - Actions
- (void)processDisplayLink:(CADisplayLink *)sender {
if (self.firstTimestamp == 0) {
self.firstTimestamp = sender.timestamp;
return;
}
CGFloat percent = (sender.timestamp - self.firstTimestamp)/self.duration;
if (percent >= 1) {
!self.progressCallback ?: self.progressCallback(percent);
[self.displayLink invalidate];
!self.completeCallback ?: self.completeCallback();
self.executing = NO;
}else {
!self.progressCallback ?: self.progressCallback(percent);
self.executing = YES;
}
}
@end

View File

@@ -0,0 +1,43 @@
//
// JXCategoryViewDefines.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
static const CGFloat JXCategoryViewAutomaticDimension = -1;
typedef void(^JXCategoryCellSelectedAnimationBlock)(CGFloat percent);
// 指示器的位置
typedef NS_ENUM(NSUInteger, JXCategoryComponentPosition) {
JXCategoryComponentPosition_Bottom,
JXCategoryComponentPosition_Top
};
// cell 被选中的类型
typedef NS_ENUM(NSUInteger, JXCategoryCellSelectedType) {
JXCategoryCellSelectedTypeUnknown, // 未知不是选中cellForRow方法里面、两个cell过渡时
JXCategoryCellSelectedTypeClick, // 点击选中
JXCategoryCellSelectedTypeCode, // 调用方法 selectItemAtIndex: 选中
JXCategoryCellSelectedTypeScroll // 通过滚动到某个 cell 选中
};
// cell 标题锚点位置
typedef NS_ENUM(NSUInteger, JXCategoryTitleLabelAnchorPointStyle) {
JXCategoryTitleLabelAnchorPointStyleCenter,
JXCategoryTitleLabelAnchorPointStyleTop,
JXCategoryTitleLabelAnchorPointStyleBottom
};
// 指示器滚动样式
typedef NS_ENUM(NSUInteger, JXCategoryIndicatorScrollStyle) {
JXCategoryIndicatorScrollStyleSimple, // 简单滚动,即从当前位置过渡到目标位置
JXCategoryIndicatorScrollStyleSameAsUserScroll // 和用户左右滚动列表时的效果一样
};
#define JXCategoryViewDeprecated(instead) NS_DEPRECATED(2_0, 2_0, 2_0, 2_0, instead)

View File

@@ -0,0 +1,18 @@
//
// UIColor+JXAdd.h
// UI系列测试
//
// Created by jiaxin on 2018/3/21.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface UIColor (JXAdd)
@property (nonatomic, assign, readonly) CGFloat jx_red;
@property (nonatomic, assign, readonly) CGFloat jx_green;
@property (nonatomic, assign, readonly) CGFloat jx_blue;
@property (nonatomic, assign, readonly) CGFloat jx_alpha;
@end

View File

@@ -0,0 +1,35 @@
//
// UIColor+JXAdd.m
// UI
//
// Created by jiaxin on 2018/3/21.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "UIColor+JXAdd.h"
@implementation UIColor (JXAdd)
- (CGFloat)jx_red {
CGFloat r = 0, g, b, a;
[self getRed:&r green:&g blue:&b alpha:&a];
return r;
}
- (CGFloat)jx_green {
CGFloat r, g = 0, b, a;
[self getRed:&r green:&g blue:&b alpha:&a];
return g;
}
- (CGFloat)jx_blue {
CGFloat r, g, b = 0, a;
[self getRed:&r green:&g blue:&b alpha:&a];
return b;
}
- (CGFloat)jx_alpha {
return CGColorGetAlpha(self.CGColor);
}
@end

View File

@@ -0,0 +1,13 @@
//
// JXCategoryDotCell.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryTitleCell.h"
@interface JXCategoryDotCell : JXCategoryTitleCell
@end

View File

@@ -0,0 +1,64 @@
//
// JXCategoryDotCell.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryDotCell.h"
#import "JXCategoryDotCellModel.h"
@interface JXCategoryDotCell ()
@property (nonatomic, strong) UIView *dot;
@end
@implementation JXCategoryDotCell
- (void)initializeViews {
[super initializeViews];
_dot = [[UIView alloc] init];
[self.contentView addSubview:self.dot];
self.dot.translatesAutoresizingMaskIntoConstraints = NO;
}
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel {
[super reloadData:cellModel];
JXCategoryDotCellModel *myCellModel = (JXCategoryDotCellModel *)cellModel;
self.dot.hidden = !myCellModel.dotHidden;
self.dot.backgroundColor = myCellModel.dotColor;
self.dot.layer.cornerRadius = myCellModel.dotCornerRadius;
[NSLayoutConstraint deactivateConstraints:self.dot.constraints];
[self.dot.widthAnchor constraintEqualToConstant:myCellModel.dotSize.width].active = YES;
[self.dot.heightAnchor constraintEqualToConstant:myCellModel.dotSize.height].active = YES;
switch (myCellModel.relativePosition) {
case JXCategoryDotRelativePosition_TopLeft:
{
[self.dot.centerXAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor constant:myCellModel.dotOffset.x].active = YES;
[self.dot.centerYAnchor constraintEqualToAnchor:self.titleLabel.topAnchor constant:myCellModel.dotOffset.y].active = YES;
}
break;
case JXCategoryDotRelativePosition_TopRight:
{
[self.dot.centerXAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor constant:myCellModel.dotOffset.x].active = YES;
[self.dot.centerYAnchor constraintEqualToAnchor:self.titleLabel.topAnchor constant:myCellModel.dotOffset.y].active = YES;
}
break;
case JXCategoryDotRelativePosition_BottomLeft:
{
[self.dot.centerXAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor constant:myCellModel.dotOffset.x].active = YES;
[self.dot.centerYAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:myCellModel.dotOffset.y].active = YES;
}
break;
case JXCategoryDotRelativePosition_BottomRight:
{
[self.dot.centerXAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor constant:myCellModel.dotOffset.x].active = YES;
[self.dot.centerYAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:myCellModel.dotOffset.y].active = YES;
}
break;
}
}
@end

View File

@@ -0,0 +1,27 @@
//
// JXCategoryDotCellModel.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryTitleCellModel.h"
typedef NS_ENUM(NSUInteger, JXCategoryDotRelativePosition) {
JXCategoryDotRelativePosition_TopLeft = 0,
JXCategoryDotRelativePosition_TopRight,
JXCategoryDotRelativePosition_BottomLeft,
JXCategoryDotRelativePosition_BottomRight,
};
@interface JXCategoryDotCellModel : JXCategoryTitleCellModel
@property (nonatomic, assign) BOOL dotHidden;
@property (nonatomic, assign) JXCategoryDotRelativePosition relativePosition;
@property (nonatomic, assign) CGSize dotSize;
@property (nonatomic, assign) CGFloat dotCornerRadius;
@property (nonatomic, strong) UIColor *dotColor;
@property (nonatomic, assign) CGPoint dotOffset;
@end

View File

@@ -0,0 +1,13 @@
//
// JXCategoryDotCellModel.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryDotCellModel.h"
@implementation JXCategoryDotCellModel
@end

View File

@@ -0,0 +1,30 @@
//
// JXCategoryDotView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryTitleView.h"
#import "JXCategoryDotCell.h"
#import "JXCategoryDotCellModel.h"
@interface JXCategoryDotView : JXCategoryTitleView
//相对于titleLabel的位置默认JXCategoryDotRelativePosition_TopRight
@property (nonatomic, assign) JXCategoryDotRelativePosition relativePosition;
//@[@(布尔值)]数组,控制红点是否显示
@property (nonatomic, strong) NSArray <NSNumber *> *dotStates;
//红点的尺寸。默认CGSizeMake(10, 10)
@property (nonatomic, assign) CGSize dotSize;
//红点的圆角值。默认JXCategoryViewAutomaticDimensionself.dotSize.height/2
@property (nonatomic, assign) CGFloat dotCornerRadius;
//红点的颜色。默认:[UIColor redColor]
@property (nonatomic, strong) UIColor *dotColor;
/**
红点 x,y方向的偏移 +值:水平方向向右,竖直方向向下)
*/
@property (nonatomic, assign) CGPoint dotOffset;
@end

View File

@@ -0,0 +1,52 @@
//
// JXCategoryDotView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryDotView.h"
@implementation JXCategoryDotView
- (void)initializeData {
[super initializeData];
_relativePosition = JXCategoryDotRelativePosition_TopRight;
_dotSize = CGSizeMake(10, 10);
_dotCornerRadius = JXCategoryViewAutomaticDimension;
_dotColor = [UIColor redColor];
_dotOffset = CGPointZero;
}
- (Class)preferredCellClass {
return [JXCategoryDotCell class];
}
- (void)refreshDataSource {
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:self.titles.count];
for (int i = 0; i < self.titles.count; i++) {
JXCategoryDotCellModel *cellModel = [[JXCategoryDotCellModel alloc] init];
[tempArray addObject:cellModel];
}
self.dataSource = [NSArray arrayWithArray:tempArray];
}
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index {
[super refreshCellModel:cellModel index:index];
JXCategoryDotCellModel *myCellModel = (JXCategoryDotCellModel *)cellModel;
myCellModel.dotHidden = [self.dotStates[index] boolValue];
myCellModel.relativePosition = self.relativePosition;
myCellModel.dotSize = self.dotSize;
myCellModel.dotColor = self.dotColor;
myCellModel.dotOffset = self.dotOffset;
if (self.dotCornerRadius == JXCategoryViewAutomaticDimension) {
myCellModel.dotCornerRadius = self.dotSize.height/2;
}else {
myCellModel.dotCornerRadius = self.dotCornerRadius;
}
}
@end

View File

@@ -0,0 +1,15 @@
//
// JXCategoryImageCell.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorCell.h"
@interface JXCategoryImageCell : JXCategoryIndicatorCell
@property (nonatomic, strong) UIImageView *imageView;
@end

View File

@@ -0,0 +1,97 @@
//
// JXCategoryImageCell.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryImageCell.h"
#import "JXCategoryImageCellModel.h"
@interface JXCategoryImageCell()
@property (nonatomic, strong) id currentImageInfo;
@property (nonatomic, strong) NSString *currentImageName;
@property (nonatomic, strong) NSURL *currentImageURL;
@end
@implementation JXCategoryImageCell
- (void)prepareForReuse {
[super prepareForReuse];
self.currentImageInfo = nil;
self.currentImageName = nil;
self.currentImageURL = nil;
}
- (void)initializeViews {
[super initializeViews];
_imageView = [[UIImageView alloc] init];
_imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.contentView addSubview:_imageView];
}
- (void)layoutSubviews {
[super layoutSubviews];
JXCategoryImageCellModel *myCellModel = (JXCategoryImageCellModel *)self.cellModel;
self.imageView.bounds = CGRectMake(0, 0, myCellModel.imageSize.width, myCellModel.imageSize.height);
self.imageView.center = self.contentView.center;
if (myCellModel.imageCornerRadius && (myCellModel.imageCornerRadius != 0)) {
self.imageView.layer.cornerRadius = myCellModel.imageCornerRadius;
self.imageView.layer.masksToBounds = YES;
}
}
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel {
[super reloadData:cellModel];
JXCategoryImageCellModel *myCellModel = (JXCategoryImageCellModel *)cellModel;
//`- (void)reloadData:(JXCategoryBaseCellModel *)cellModel`
if (myCellModel.loadImageBlock != nil) {
id currentImageInfo = myCellModel.imageInfo;
if (myCellModel.isSelected) {
currentImageInfo = myCellModel.selectedImageInfo;
}
if (currentImageInfo && ![currentImageInfo isEqual:self.currentImageInfo]) {
self.currentImageInfo = currentImageInfo;
if (myCellModel.loadImageBlock) {
myCellModel.loadImageBlock(self.imageView, currentImageInfo);
}
}
} else {
NSString *currentImageName;
NSURL *currentImageURL;
if (myCellModel.imageName) {
currentImageName = myCellModel.imageName;
} else if (myCellModel.imageURL) {
currentImageURL = myCellModel.imageURL;
}
if (myCellModel.isSelected) {
if (myCellModel.selectedImageName) {
currentImageName = myCellModel.selectedImageName;
} else if (myCellModel.selectedImageURL) {
currentImageURL = myCellModel.selectedImageURL;
}
}
if (currentImageName && ![currentImageName isEqualToString:self.currentImageName]) {
self.currentImageName = currentImageName;
self.imageView.image = [UIImage imageNamed:currentImageName];
} else if (currentImageURL && ![currentImageURL.absoluteString isEqualToString:self.currentImageURL.absoluteString]) {
self.currentImageURL = currentImageURL;
if (myCellModel.loadImageCallback) {
myCellModel.loadImageCallback(self.imageView, currentImageURL);
}
}
}
if (myCellModel.isImageZoomEnabled) {
self.imageView.transform = CGAffineTransformMakeScale(myCellModel.imageZoomScale, myCellModel.imageZoomScale);
}else {
self.imageView.transform = CGAffineTransformIdentity;
}
}
@end

View File

@@ -0,0 +1,32 @@
//
// JXCategoryImageCellModel.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorCellModel.h"
@interface JXCategoryImageCellModel : JXCategoryIndicatorCellModel
@property (nonatomic, strong) id imageInfo;
@property (nonatomic, strong) id selectedImageInfo;
@property (nonatomic, copy) void(^loadImageBlock)(UIImageView *imageView, id info);
@property (nonatomic, assign) CGSize imageSize;
@property (nonatomic, assign) CGFloat imageCornerRadius;
@property (nonatomic, assign, getter=isImageZoomEnabled) BOOL imageZoomEnabled;
@property (nonatomic, assign) CGFloat imageZoomScale;
/// 以下属性将会被弃用
@property (nonatomic, copy) NSString *imageName; //加载bundle内的图片
@property (nonatomic, strong) NSURL *imageURL; //图片URL
@property (nonatomic, copy) NSString *selectedImageName;
@property (nonatomic, strong) NSURL *selectedImageURL;
@property (nonatomic, copy) void(^loadImageCallback)(UIImageView *imageView, NSURL *imageURL);
@end

View File

@@ -0,0 +1,13 @@
//
// JXCategoryImageCellModel.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryImageCellModel.h"
@implementation JXCategoryImageCellModel
@end

View File

@@ -0,0 +1,32 @@
//
// JXCategoryImageView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorView.h"
#import "JXCategoryImageCell.h"
#import "JXCategoryImageCellModel.h"
@interface JXCategoryImageView : JXCategoryIndicatorView
//imageInfo数组可以传入imageName字符串或者image的URL地址等然后会通过loadImageBlock透传回来把imageView对于图片的加载过程完全交给使用者决定。
@property (nonatomic, strong) NSArray <id>*imageInfoArray;
@property (nonatomic, strong) NSArray <id>*selectedImageInfoArray;
@property (nonatomic, copy) void(^loadImageBlock)(UIImageView *imageView, id info);
@property (nonatomic, assign) CGSize imageSize; //默认值为 CGSizeMake(20, 20)
@property (nonatomic, assign) CGFloat imageCornerRadius; //图片圆角
@property (nonatomic, assign, getter=isImageZoomEnabled) BOOL imageZoomEnabled; //默认值为 NO
@property (nonatomic, assign) CGFloat imageZoomScale; //默认值为 1.2imageZoomEnabled 为 YES 时才生效
//下面的属性将会被弃用,请使用`imageInfoArray`、`selectedImageInfoArray`、`loadImageBlock`属性完成需求。
@property (nonatomic, strong) NSArray <NSString *>*imageNames;
@property (nonatomic, strong) NSArray <NSURL *>*imageURLs;
@property (nonatomic, strong) NSArray <NSString *>*selectedImageNames;
@property (nonatomic, strong) NSArray <NSURL *>*selectedImageURLs;
@property (nonatomic, copy) void(^loadImageCallback)(UIImageView *imageView, NSURL *imageURL); //使用imageURL从远端下载图片进行加载建议使用SDWebImage等第三方库进行下载。
@end

View File

@@ -0,0 +1,104 @@
//
// JXCategoryImageView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/20.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryImageView.h"
#import "JXCategoryFactory.h"
@implementation JXCategoryImageView
- (void)dealloc {
self.loadImageBlock = nil;
self.loadImageCallback = nil;
}
- (void)initializeData {
[super initializeData];
_imageSize = CGSizeMake(20, 20);
_imageZoomEnabled = NO;
_imageZoomScale = 1.2;
_imageCornerRadius = 0;
}
- (Class)preferredCellClass {
return [JXCategoryImageCell class];
}
- (void)refreshDataSource {
NSUInteger count = 0;
if (self.imageInfoArray.count > 0) {
count = self.imageInfoArray.count;
}else if (self.imageNames.count > 0) {
count = self.imageNames.count;
}else {
count = self.imageURLs.count;
}
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; i++) {
JXCategoryImageCellModel *cellModel = [[JXCategoryImageCellModel alloc] init];
[tempArray addObject:cellModel];
}
self.dataSource = [NSArray arrayWithArray:tempArray];
}
- (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel {
[super refreshSelectedCellModel:selectedCellModel unselectedCellModel:unselectedCellModel];
JXCategoryImageCellModel *myUnselectedCellModel = (JXCategoryImageCellModel *)unselectedCellModel;
myUnselectedCellModel.imageZoomScale = 1.0;
JXCategoryImageCellModel *myselectedCellModel = (JXCategoryImageCellModel *)selectedCellModel;
myselectedCellModel.imageZoomScale = self.imageZoomScale;
}
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index {
[super refreshCellModel:cellModel index:index];
JXCategoryImageCellModel *myCellModel = (JXCategoryImageCellModel *)cellModel;
myCellModel.loadImageBlock = self.loadImageBlock;
myCellModel.loadImageCallback = self.loadImageCallback;
myCellModel.imageSize = self.imageSize;
myCellModel.imageCornerRadius = self.imageCornerRadius;
if (self.imageInfoArray && self.imageInfoArray.count != 0) {
myCellModel.imageInfo = self.imageInfoArray[index];
}else if (self.imageNames && self.imageNames.count != 0) {
myCellModel.imageName = self.imageNames[index];
}else if (self.imageURLs && self.imageURLs.count != 0) {
myCellModel.imageURL = self.imageURLs[index];
}
if (self.selectedImageInfoArray && self.selectedImageInfoArray.count != 0) {
myCellModel.selectedImageInfo = self.selectedImageInfoArray[index];
}else if (self.selectedImageNames && self.selectedImageNames.count != 0) {
myCellModel.selectedImageName = self.selectedImageNames[index];
}else if (self.selectedImageURLs && self.selectedImageURLs.count != 0) {
myCellModel.selectedImageURL = self.selectedImageURLs[index];
}
myCellModel.imageZoomEnabled = self.imageZoomEnabled;
myCellModel.imageZoomScale = ((index == self.selectedIndex) ? self.imageZoomScale : 1.0);
}
- (void)refreshLeftCellModel:(JXCategoryBaseCellModel *)leftCellModel rightCellModel:(JXCategoryBaseCellModel *)rightCellModel ratio:(CGFloat)ratio {
[super refreshLeftCellModel:leftCellModel rightCellModel:rightCellModel ratio:ratio];
JXCategoryImageCellModel *leftModel = (JXCategoryImageCellModel *)leftCellModel;
JXCategoryImageCellModel *rightModel = (JXCategoryImageCellModel *)rightCellModel;
if (self.isImageZoomEnabled) {
leftModel.imageZoomScale = [JXCategoryFactory interpolationFrom:self.imageZoomScale to:1.0 percent:ratio];
rightModel.imageZoomScale = [JXCategoryFactory interpolationFrom:1.0 to:self.imageZoomScale percent:ratio];
}
}
- (CGFloat)preferredCellWidthAtIndex:(NSInteger)index {
if (self.cellWidth == JXCategoryViewAutomaticDimension) {
return self.imageSize.width;
}
return self.cellWidth;
}
@end

View File

@@ -0,0 +1,14 @@
//
// JXCategoryIndicatorBackgroundView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorComponentView.h"
/// BackgroundView 样式的指示器
@interface JXCategoryIndicatorBackgroundView : JXCategoryIndicatorComponentView
@end

View File

@@ -0,0 +1,101 @@
//
// JXCategoryIndicatorBackgroundView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorBackgroundView.h"
#import "JXCategoryFactory.h"
@implementation JXCategoryIndicatorBackgroundView
#pragma mark - Initialize
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configureDefaulteValue];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self configureDefaulteValue];
}
return self;
}
- (void)configureDefaulteValue {
self.indicatorWidth = JXCategoryViewAutomaticDimension;
self.indicatorHeight = JXCategoryViewAutomaticDimension;
self.indicatorCornerRadius = JXCategoryViewAutomaticDimension;
self.indicatorColor = [UIColor lightGrayColor];
self.indicatorWidthIncrement = 10;
}
#pragma mark - JXCategoryIndicatorProtocol
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model {
self.layer.cornerRadius = [self indicatorCornerRadiusValue:model.selectedCellFrame];
self.backgroundColor = self.indicatorColor;
CGFloat width = [self indicatorWidthValue:model.selectedCellFrame];
CGFloat height = [self indicatorHeightValue:model.selectedCellFrame];
CGFloat x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - width)/2;
CGFloat y = (model.selectedCellFrame.size.height - height)/2 - self.verticalMargin;
self.frame = CGRectMake(x, y, width, height);
}
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model {
CGRect rightCellFrame = model.rightCellFrame;
CGRect leftCellFrame = model.leftCellFrame;
CGFloat percent = model.percent;
CGFloat targetX = 0;
CGFloat targetWidth = [self indicatorWidthValue:leftCellFrame];
if (percent == 0) {
targetX = leftCellFrame.origin.x + (leftCellFrame.size.width - targetWidth)/2.0;
}else {
CGFloat leftWidth = targetWidth;
CGFloat rightWidth = [self indicatorWidthValue:rightCellFrame];
CGFloat leftX = leftCellFrame.origin.x + (leftCellFrame.size.width - leftWidth)/2;
CGFloat rightX = rightCellFrame.origin.x + (rightCellFrame.size.width - rightWidth)/2;
targetX = [JXCategoryFactory interpolationFrom:leftX to:rightX percent:percent];
if (self.indicatorWidth == JXCategoryViewAutomaticDimension) {
targetWidth = [JXCategoryFactory interpolationFrom:leftWidth to:rightWidth percent:percent];
}
}
//frame12
if (self.isScrollEnabled == YES || (self.isScrollEnabled == NO && percent == 0)) {
CGRect toFrame = self.frame;
toFrame.origin.x = targetX;
toFrame.size.width = targetWidth;
self.frame = toFrame;
}
}
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model {
CGFloat width = [self indicatorWidthValue:model.selectedCellFrame];
CGRect toFrame = self.frame;
toFrame.origin.x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - width)/2;
toFrame.size.width = width;
if (self.isScrollEnabled) {
[UIView animateWithDuration:self.scrollAnimationDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
self.frame = toFrame;
} completion:^(BOOL finished) {
}];
}else {
self.frame = toFrame;
}
}
@end

View File

@@ -0,0 +1,17 @@
//
// JXCategoryIndicatorBallView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/21.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorComponentView.h"
/// QQ 小红点样式的指示器
@interface JXCategoryIndicatorBallView : JXCategoryIndicatorComponentView
// 球沿的 X 轴方向上的偏移量。默认值为 20
@property (nonatomic, assign) CGFloat ballScrollOffsetX;
@end

View File

@@ -0,0 +1,199 @@
//
// JXCategoryIndicatorBallView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/21.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorBallView.h"
#import "JXCategoryFactory.h"
@interface JXCategoryIndicatorBallView ()
@property (nonatomic, strong) UIView *smallBall;
@property (nonatomic, strong) UIView *bigBall;
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
@end
@implementation JXCategoryIndicatorBallView
#pragma mark - Initialize
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configureIndicatorBall];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self configureIndicatorBall];
}
return self;
}
- (void)configureIndicatorBall {
self.indicatorWidth = 15;
self.indicatorHeight = 15;
_ballScrollOffsetX = 20;
_smallBall = [[UIView alloc] init];
[self addSubview:self.smallBall];
_bigBall = [[UIView alloc] init];
[self addSubview:self.bigBall];
_shapeLayer = [CAShapeLayer layer];
[self.layer addSublayer:self.shapeLayer];
}
#pragma mark - JXCategoryIndicatorProtocol
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model {
CGFloat ballWidth = [self indicatorWidthValue:model.selectedCellFrame];
CGFloat ballHeight = [self indicatorHeightValue:model.selectedCellFrame];
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.shapeLayer.fillColor = self.indicatorColor.CGColor;
[CATransaction commit];
self.smallBall.backgroundColor = self.indicatorColor;
self.smallBall.layer.cornerRadius = ballHeight/2;
self.bigBall.backgroundColor = self.indicatorColor;
self.bigBall.layer.cornerRadius = ballHeight/2;
CGFloat x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - ballWidth)/2;
CGFloat y = self.superview.bounds.size.height - ballHeight - self.verticalMargin;
if (self.componentPosition == JXCategoryComponentPosition_Top) {
y = self.verticalMargin;
}
self.smallBall.frame = CGRectMake(x, y, ballWidth, ballHeight);
self.bigBall.frame = CGRectMake(x, y, ballWidth, ballHeight);
}
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model {
CGFloat ballWidth = [self indicatorWidthValue:model.leftCellFrame];
CGFloat ballHeight = [self indicatorHeightValue:model.leftCellFrame];
CGRect rightCellFrame = model.rightCellFrame;
CGRect leftCellFrame = model.leftCellFrame;
CGFloat percent = model.percent;
CGFloat targetXOfBigBall = 0;
CGFloat targetXOfSmallBall = leftCellFrame.origin.x + (leftCellFrame.size.width - ballWidth)/2;
CGFloat targetWidthOfSmallBall = ballWidth;
if (percent == 0) {
targetXOfBigBall = leftCellFrame.origin.x + (leftCellFrame.size.width - ballWidth)/2.0;
targetXOfSmallBall = leftCellFrame.origin.x + (leftCellFrame.size.width - targetWidthOfSmallBall)/2.0;
}else {
CGFloat leftX = leftCellFrame.origin.x + (leftCellFrame.size.width - ballWidth)/2;
CGFloat rightX = rightCellFrame.origin.x + (rightCellFrame.size.width - ballWidth)/2;
//50%bigBallxsmallBall50%bigBallxsmallBallsmallBallx
if (percent <= 0.5) {
targetXOfBigBall = [JXCategoryFactory interpolationFrom:leftX to:(rightX - self.ballScrollOffsetX) percent:percent*2];
targetWidthOfSmallBall = [JXCategoryFactory interpolationFrom:ballWidth to:ballWidth/2 percent:percent*2];
}else {
targetXOfBigBall = [JXCategoryFactory interpolationFrom:(rightX - self.ballScrollOffsetX) to:rightX percent:(percent - 0.5)*2];
targetWidthOfSmallBall = [JXCategoryFactory interpolationFrom:ballWidth/2 to:0 percent:(percent - 0.5)*2];
targetXOfSmallBall = [JXCategoryFactory interpolationFrom:leftX to:rightX percent:(percent - 0.5)*2];
}
}
//frame12
if (self.isScrollEnabled == YES || (self.isScrollEnabled == NO && percent == 0)) {
CGRect bigBallFrame = self.bigBall.frame;
bigBallFrame.origin.x = targetXOfBigBall;
self.bigBall.frame = bigBallFrame;
self.bigBall.layer.cornerRadius = bigBallFrame.size.height/2;
CGFloat targetYOfSmallBall = self.superview.bounds.size.height - ballHeight/2 - targetWidthOfSmallBall/2 - self.verticalMargin;
if (self.componentPosition == JXCategoryComponentPosition_Top) {
targetYOfSmallBall = ballHeight/2 - targetWidthOfSmallBall/2 + self.verticalMargin;
}
self.smallBall.frame = CGRectMake(targetXOfSmallBall, targetYOfSmallBall, targetWidthOfSmallBall, targetWidthOfSmallBall);
self.smallBall.layer.cornerRadius = targetWidthOfSmallBall/2;
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.shapeLayer.path = [self getBezierPathWithSmallCir:self.smallBall andBigCir:self.bigBall].CGPath;
[CATransaction commit];
}
}
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model {
CGFloat ballWidth = [self indicatorWidthValue:model.selectedCellFrame];
CGFloat ballHeight = [self indicatorHeightValue:model.selectedCellFrame];
CGFloat x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - ballWidth)/2;
CGFloat y = self.superview.bounds.size.height - ballHeight - self.verticalMargin;
if (self.componentPosition == JXCategoryComponentPosition_Top) {
y = self.verticalMargin;
}
CGRect toFrame = CGRectMake(x, y, ballWidth, ballHeight);
if (self.isScrollEnabled) {
[UIView animateWithDuration:self.scrollAnimationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.smallBall.frame = toFrame;
self.bigBall.frame = toFrame;
self.smallBall.layer.cornerRadius = ballHeight/2;
self.bigBall.layer.cornerRadius = ballHeight/2;
} completion:^(BOOL finished) {
}];
}else {
self.smallBall.frame = toFrame;
self.bigBall.frame = toFrame;
self.smallBall.layer.cornerRadius = ballHeight/2;
self.bigBall.layer.cornerRadius = ballHeight/2;
}
}
- (UIBezierPath *)getBezierPathWithSmallCir:(UIView *)smallCir andBigCir:(UIView *)bigCir{
//
if (bigCir.frame.size.width < smallCir.frame.size.width) {
UIView *view = bigCir;
bigCir = smallCir;
smallCir = view;
}
//
CGFloat d = self.bigBall.center.x - self.smallBall.center.x;
if (d == 0) {
return nil;
}
CGFloat x1 = smallCir.center.x;
CGFloat y1 = smallCir.center.y;
CGFloat r1 = smallCir.bounds.size.width/2;
//
CGFloat x2 = bigCir.center.x;
CGFloat y2 = bigCir.center.y;
CGFloat r2 = bigCir.bounds.size.width/2;
//
CGFloat sinA = (y2 - y1)/d;
CGFloat cosA = (x2 - x1)/d;
//
CGPoint pointA = CGPointMake(x1 - sinA*r1, y1 + cosA * r1);
CGPoint pointB = CGPointMake(x1 + sinA*r1, y1 - cosA * r1);
CGPoint pointC = CGPointMake(x2 + sinA*r2, y2 - cosA * r2);
CGPoint pointD = CGPointMake(x2 - sinA*r2, y2 + cosA * r2);
// 便线
CGPoint pointO = CGPointMake(pointA.x + d / 2 * cosA , pointA.y + d / 2 * sinA);
CGPoint pointP = CGPointMake(pointB.x + d / 2 * cosA , pointB.y + d / 2 * sinA);
//
UIBezierPath *path =[UIBezierPath bezierPath];
[path moveToPoint:pointA];
[path addLineToPoint:pointB];
[path addQuadCurveToPoint:pointC controlPoint:pointP];
[path addLineToPoint:pointD];
[path addQuadCurveToPoint:pointA controlPoint:pointO];
return path;
}
@end

View File

@@ -0,0 +1,113 @@
//
// JXCategoryComponentBaseView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "JXCategoryIndicatorProtocol.h"
#import "JXCategoryViewDefines.h"
@interface JXCategoryIndicatorComponentView : UIView <JXCategoryIndicatorProtocol>
/**
指示器的位置
可设置的枚举类型:
- 底部JXCategoryComponentPosition_Bottom
- 顶部JXCategoryComponentPosition_Top
*/
@property (nonatomic, assign) JXCategoryComponentPosition componentPosition;
/**
指示器的宽度
默认值为 JXCategoryViewAutomaticDimension表示与 cell 的宽度相等)。
内部通过 `- (CGFloat)indicatorWidthValue:(CGRect)cellFrame` 方法获取实际的值。
*/
@property (nonatomic, assign) CGFloat indicatorWidth;
/**
指示器的宽度增量
例如:需求是指示器宽度比 cell 宽度多 10pt。就可以将该属性赋值为 10。
最终指示器的宽度 = indicatorWidth + indicatorWidthIncrement。
*/
@property (nonatomic, assign) CGFloat indicatorWidthIncrement;
/**
指示器的高度
默认值为 3。
内部通过 `- (CGFloat)indicatorHeightValue:(CGRect)cellFrame` 方法获取实际的值。
*/
@property (nonatomic, assign) CGFloat indicatorHeight;
/**
指示器的 CornerRadius 圆角半径
默认值为 JXCategoryViewAutomaticDimension (等于 indicatorHeight/2
内部通过 `- (CGFloat)indicatorCornerRadiusValue:(CGRect)cellFrame` 方法获取实际的值。
*/
@property (nonatomic, assign) CGFloat indicatorCornerRadius;
/**
指示器的颜色
*/
@property (nonatomic, strong) UIColor *indicatorColor;
/**
指示器在垂直方向上的偏移量
数值越大越靠近中心。默认值为 0。
*/
@property (nonatomic, assign) CGFloat verticalMargin;
/**
是否允许手势滚动
点击切换的时候,是否允许滚动,默认值为 YES。
*/
@property (nonatomic, assign, getter=isScrollEnabled) BOOL scrollEnabled;
/**
指示器滚动样式
点击切换的时候,如果允许滚动,分为简单滚动和复杂滚动。
默认值为 JXCategoryIndicatorScrollStyleSimple
目前仅JXCategoryIndicatorLineView、JXCategoryIndicatorDotLineView支持其他子类暂不支持。
*/
@property (nonatomic, assign) JXCategoryIndicatorScrollStyle scrollStyle;
/**
滚动动画的时间,默认值为 0.25s
*/
@property (nonatomic, assign) NSTimeInterval scrollAnimationDuration;
/**
传入 cellFrame 获取指示器的最终宽度
@param cellFrame cellFrame
@return 指示器的最终宽度
*/
- (CGFloat)indicatorWidthValue:(CGRect)cellFrame;
/**
传入 cellFrame 获取指示器的最终高度
@param cellFrame cellFrame
@return 指示器的最终高度
*/
- (CGFloat)indicatorHeightValue:(CGRect)cellFrame;
/**
传入 cellFrame 获取指示器的最终圆角
@param cellFrame cellFrame
@return 指示器的最终圆角
*/
- (CGFloat)indicatorCornerRadiusValue:(CGRect)cellFrame;
@end

View File

@@ -0,0 +1,81 @@
//
// JXCategoryComponentBaseView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorComponentView.h"
@implementation JXCategoryIndicatorComponentView
#pragma mark - Initialize
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configureDefaultValue];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self configureDefaultValue];
}
return self;
}
- (void)configureDefaultValue {
_componentPosition = JXCategoryComponentPosition_Bottom;
_scrollEnabled = YES;
_verticalMargin = 0;
_scrollAnimationDuration = 0.25;
_indicatorWidth = JXCategoryViewAutomaticDimension;
_indicatorWidthIncrement = 0;
_indicatorHeight = 3;
_indicatorCornerRadius = JXCategoryViewAutomaticDimension;
_indicatorColor = [UIColor redColor];
_scrollStyle = JXCategoryIndicatorScrollStyleSimple;
}
#pragma mark - Public
- (CGFloat)indicatorWidthValue:(CGRect)cellFrame {
if (self.indicatorWidth == JXCategoryViewAutomaticDimension) {
return cellFrame.size.width + self.indicatorWidthIncrement;
}
return self.indicatorWidth + self.indicatorWidthIncrement;
}
- (CGFloat)indicatorHeightValue:(CGRect)cellFrame {
if (self.indicatorHeight == JXCategoryViewAutomaticDimension) {
return cellFrame.size.height;
}
return self.indicatorHeight;
}
- (CGFloat)indicatorCornerRadiusValue:(CGRect)cellFrame {
if (self.indicatorCornerRadius == JXCategoryViewAutomaticDimension) {
return [self indicatorHeightValue:cellFrame]/2;
}
return self.indicatorCornerRadius;
}
#pragma mark - JXCategoryIndicatorProtocol
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model {
}
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model {
}
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model {
}
@end

View File

@@ -0,0 +1,17 @@
//
// JXCategoryIndicatorDotLineView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/22.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorComponentView.h"
/// 点线效果的指示器
@interface JXCategoryIndicatorDotLineView : JXCategoryIndicatorComponentView
// 线状态的最大宽度,默认值为 50
@property (nonatomic, assign) CGFloat lineWidth;
@end

View File

@@ -0,0 +1,148 @@
//
// JXCategoryIndicatorDotLineView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/22.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorDotLineView.h"
#import "JXCategoryFactory.h"
#import "JXCategoryViewAnimator.h"
@interface JXCategoryIndicatorDotLineView ()
@property (nonatomic, strong) JXCategoryViewAnimator *animator;
@end
@implementation JXCategoryIndicatorDotLineView
#pragma mark - Initialize
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configureDefaulteValue];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self configureDefaulteValue];
}
return self;
}
- (void)configureDefaulteValue {
self.indicatorWidth = 10;
self.indicatorHeight = 10;
_lineWidth = 50;
}
#pragma mark - JXCategoryIndicatorProtocol
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model {
CGFloat dotWidth = [self indicatorWidthValue:model.selectedCellFrame];
CGFloat dotHeight = [self indicatorHeightValue:model.selectedCellFrame];
self.backgroundColor = self.indicatorColor;
self.layer.cornerRadius = [self indicatorHeightValue:model.selectedCellFrame]/2;
CGFloat x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - dotWidth)/2;
CGFloat y = self.superview.bounds.size.height - dotHeight - self.verticalMargin;
if (self.componentPosition == JXCategoryComponentPosition_Top) {
y = self.verticalMargin;
}
self.frame = CGRectMake(x, y, dotWidth, dotHeight);
}
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model {
if (self.animator.isExecuting) {
[self.animator invalid];
self.animator = nil;
}
CGFloat dotWidth = [self indicatorWidthValue:model.selectedCellFrame];
CGRect rightCellFrame = model.rightCellFrame;
CGRect leftCellFrame = model.leftCellFrame;
CGFloat percent = model.percent;
CGFloat targetX = 0;
CGFloat targetWidth = dotWidth;
CGFloat leftWidth = dotWidth;
CGFloat rightWidth = dotWidth;
CGFloat leftX = leftCellFrame.origin.x + (leftCellFrame.size.width - leftWidth)/2;
CGFloat rightX = rightCellFrame.origin.x + (rightCellFrame.size.width - rightWidth)/2;
CGFloat centerX = leftX + (rightX - leftX - self.lineWidth)/2;
//50%x50%xwidth
if (percent <= 0.5) {
targetX = [JXCategoryFactory interpolationFrom:leftX to:centerX percent:percent*2];
targetWidth = [JXCategoryFactory interpolationFrom:dotWidth to:self.lineWidth percent:percent*2];
}else {
targetX = [JXCategoryFactory interpolationFrom:centerX to:rightX percent:(percent - 0.5)*2];
targetWidth = [JXCategoryFactory interpolationFrom:self.lineWidth to:dotWidth percent:(percent - 0.5)*2];
}
//frame12
if (self.isScrollEnabled == YES || (self.isScrollEnabled == NO && percent == 0)) {
CGRect frame = self.frame;
frame.origin.x = targetX;
frame.size.width = targetWidth;
self.frame = frame;
}
}
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model {
CGFloat dotWidth = [self indicatorWidthValue:model.selectedCellFrame];
CGFloat x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - dotWidth)/2;
CGRect targetIndicatorFrame = self.frame;
targetIndicatorFrame.origin.x = x;
if (self.isScrollEnabled) {
if (self.scrollStyle == JXCategoryIndicatorScrollStyleSameAsUserScroll && (model.selectedType == JXCategoryCellSelectedTypeClick | model.selectedType == JXCategoryCellSelectedTypeCode)) {
if (self.animator.isExecuting) {
[self.animator invalid];
self.animator = nil;
}
CGFloat leftX = 0;
CGFloat rightX = 0;
BOOL isNeedReversePercent = NO;
if (self.frame.origin.x > model.selectedCellFrame.origin.x) {
leftX = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - dotWidth)/2;;
rightX = self.frame.origin.x;
isNeedReversePercent = YES;
}else {
leftX = self.frame.origin.x;
rightX = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - dotWidth)/2;
}
CGFloat centerX = leftX + (rightX - leftX - self.lineWidth)/2;
__weak typeof(self) weakSelf = self;
self.animator = [[JXCategoryViewAnimator alloc] init];
self.animator.progressCallback = ^(CGFloat percent) {
if (isNeedReversePercent) {
percent = 1 - percent;
}
CGFloat targetX = 0;
CGFloat targetWidth = 0;
if (percent <= 0.5) {
targetX = [JXCategoryFactory interpolationFrom:leftX to:centerX percent:percent*2];
targetWidth = [JXCategoryFactory interpolationFrom:dotWidth to:self.lineWidth percent:percent*2];
}else {
targetX = [JXCategoryFactory interpolationFrom:centerX to:rightX percent:(percent - 0.5)*2];
targetWidth = [JXCategoryFactory interpolationFrom:self.lineWidth to:dotWidth percent:(percent - 0.5)*2];
}
CGRect toFrame = weakSelf.frame;
toFrame.origin.x = targetX;
toFrame.size.width = targetWidth;
weakSelf.frame = toFrame;
};
[self.animator start];
}else if (self.scrollStyle == JXCategoryIndicatorScrollStyleSimple) {
[UIView animateWithDuration:self.scrollAnimationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.frame = targetIndicatorFrame;
} completion: nil];
}
}else {
self.frame = targetIndicatorFrame;
}
}
@end

View File

@@ -0,0 +1,20 @@
//
// JXCategoryIndicatorImageView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorComponentView.h"
@interface JXCategoryIndicatorImageView : JXCategoryIndicatorComponentView
// 指示器图片
@property (nonatomic, strong, readonly) UIImageView *indicatorImageView;
// 图片是否开启滚动,默认值为 NO
@property (nonatomic, assign) BOOL indicatorImageViewRollEnabled;
// 图片的尺寸,默认值为 CGSizeMake(30, 20)
@property (nonatomic, assign) CGSize indicatorImageViewSize;
@end

View File

@@ -0,0 +1,117 @@
//
// JXCategoryIndicatorImageView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorImageView.h"
#import "JXCategoryFactory.h"
@implementation JXCategoryIndicatorImageView
#pragma mark - Initialize
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupIndicatorImageView];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self setupIndicatorImageView];
}
return self;
}
- (void)setupIndicatorImageView {
_indicatorImageViewSize = CGSizeMake(30, 20);
_indicatorImageViewRollEnabled = NO;
_indicatorImageView = [[UIImageView alloc] init];
self.indicatorImageView.frame = CGRectMake(0, 0, self.indicatorImageViewSize.width, self.indicatorImageViewSize.height);
self.indicatorImageView.contentMode = UIViewContentModeScaleAspectFit;
[self addSubview:self.indicatorImageView];
}
#pragma mark - Custom Accessors
- (void)setIndicatorImageViewSize:(CGSize)indicatorImageViewSize {
_indicatorImageViewSize = indicatorImageViewSize;
self.indicatorImageView.frame = CGRectMake(0, 0, self.indicatorImageViewSize.width, self.indicatorImageViewSize.height);
}
#pragma mark - JXCategoryIndicatorProtocol
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model {
CGFloat x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - self.indicatorImageViewSize.width)/2;
CGFloat y = self.superview.bounds.size.height - self.indicatorImageViewSize.height - self.verticalMargin;
if (self.componentPosition == JXCategoryComponentPosition_Top) {
y = self.verticalMargin;
}
self.frame = CGRectMake(x, y, self.indicatorImageViewSize.width, self.indicatorImageViewSize.height);
}
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model {
CGRect rightCellFrame = model.rightCellFrame;
CGRect leftCellFrame = model.leftCellFrame;
CGFloat percent = model.percent;
CGFloat targetWidth = self.indicatorImageViewSize.width;
CGFloat targetX = 0;
if (percent == 0) {
targetX = leftCellFrame.origin.x + (leftCellFrame.size.width - targetWidth)/2.0;
}else {
CGFloat leftX = leftCellFrame.origin.x + (leftCellFrame.size.width - targetWidth)/2;
CGFloat rightX = rightCellFrame.origin.x + (rightCellFrame.size.width - targetWidth)/2;
targetX = [JXCategoryFactory interpolationFrom:leftX to:rightX percent:percent];
}
//frame12
if (self.isScrollEnabled == YES || (self.isScrollEnabled == NO && percent == 0)) {
CGRect frame = self.frame;
frame.origin.x = targetX;
self.frame = frame;
if (self.indicatorImageViewRollEnabled) {
self.indicatorImageView.transform = CGAffineTransformMakeRotation(M_PI*2*percent);
}
}
}
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model {
CGRect toFrame = self.frame;
toFrame.origin.x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - self.indicatorImageViewSize.width)/2;
if (self.isScrollEnabled) {
[UIView animateWithDuration:self.scrollAnimationDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
self.frame = toFrame;
} completion:^(BOOL finished) {
}];
if (self.indicatorImageViewRollEnabled && (model.selectedType == JXCategoryCellSelectedTypeCode || model.selectedType == JXCategoryCellSelectedTypeClick)) {
[self.indicatorImageView.layer removeAnimationForKey:@"rotate"];
CABasicAnimation *rotateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
if (model.selectedIndex > model.lastSelectedIndex) {
rotateAnimation.fromValue = @(0);
rotateAnimation.toValue = @(M_PI*2);
}else {
rotateAnimation.fromValue = @(M_PI*2);
rotateAnimation.toValue = @(0);
}
rotateAnimation.fillMode = kCAFillModeBackwards;
rotateAnimation.removedOnCompletion = YES;
rotateAnimation.duration = 0.25;
[self.indicatorImageView.layer addAnimation:rotateAnimation forKey:@"rotate"];
}
}else {
self.frame = toFrame;
}
}
@end

View File

@@ -0,0 +1,28 @@
//
// JXCategoryIndicatorLineView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorComponentView.h"
typedef NS_ENUM(NSUInteger, JXCategoryIndicatorLineStyle) {
JXCategoryIndicatorLineStyle_Normal = 0,
JXCategoryIndicatorLineStyle_Lengthen = 1,
JXCategoryIndicatorLineStyle_LengthenOffset = 2,
};
@interface JXCategoryIndicatorLineView : JXCategoryIndicatorComponentView
@property (nonatomic, assign) JXCategoryIndicatorLineStyle lineStyle;
/**
line 滚动时沿 x 轴方向上的偏移量,默认值为 10。
lineStyle 为 JXCategoryIndicatorLineStyle_LengthenOffset 有用。
*/
@property (nonatomic, assign) CGFloat lineScrollOffsetX;
@end

View File

@@ -0,0 +1,202 @@
//
// JXCategoryIndicatorLineView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorLineView.h"
#import "JXCategoryFactory.h"
#import "JXCategoryViewDefines.h"
#import "JXCategoryViewAnimator.h"
@interface JXCategoryIndicatorLineView ()
@property (nonatomic, strong) JXCategoryViewAnimator *animator;
@end
@implementation JXCategoryIndicatorLineView
#pragma mark - Initialize
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configureDefaulteValue];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self configureDefaulteValue];
}
return self;
}
- (void)configureDefaulteValue {
_lineStyle = JXCategoryIndicatorLineStyle_Normal;
_lineScrollOffsetX = 10;
self.indicatorHeight = 3;
}
#pragma mark - JXCategoryIndicatorProtocol
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model {
self.backgroundColor = self.indicatorColor;
self.layer.cornerRadius = [self indicatorCornerRadiusValue:model.selectedCellFrame];
CGFloat selectedLineWidth = [self indicatorWidthValue:model.selectedCellFrame];
CGFloat x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - selectedLineWidth)/2;
CGFloat y = self.superview.bounds.size.height - [self indicatorHeightValue:model.selectedCellFrame] - self.verticalMargin;
if (self.componentPosition == JXCategoryComponentPosition_Top) {
y = self.verticalMargin;
}
self.frame = CGRectMake(x, y, selectedLineWidth, [self indicatorHeightValue:model.selectedCellFrame]);
}
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model {
if (self.animator.isExecuting) {
[self.animator invalid];
self.animator = nil;
}
CGRect rightCellFrame = model.rightCellFrame;
CGRect leftCellFrame = model.leftCellFrame;
CGFloat percent = model.percent;
CGFloat targetX = leftCellFrame.origin.x;
CGFloat targetWidth = [self indicatorWidthValue:leftCellFrame];
CGFloat leftWidth = targetWidth;
CGFloat rightWidth = [self indicatorWidthValue:rightCellFrame];
CGFloat leftX = leftCellFrame.origin.x + (leftCellFrame.size.width - leftWidth)/2;
CGFloat rightX = rightCellFrame.origin.x + (rightCellFrame.size.width - rightWidth)/2;
if (self.lineStyle == JXCategoryIndicatorLineStyle_Normal) {
targetX = [JXCategoryFactory interpolationFrom:leftX to:rightX percent:percent];
if (self.indicatorWidth == JXCategoryViewAutomaticDimension) {
targetWidth = [JXCategoryFactory interpolationFrom:leftWidth to:rightWidth percent:percent];
}
}else if (self.lineStyle == JXCategoryIndicatorLineStyle_Lengthen) {
CGFloat maxWidth = rightX - leftX + rightWidth;
//50%width50%xwidth
if (percent <= 0.5) {
targetX = leftX;
targetWidth = [JXCategoryFactory interpolationFrom:leftWidth to:maxWidth percent:percent*2];
}else {
targetX = [JXCategoryFactory interpolationFrom:leftX to:rightX percent:(percent - 0.5)*2];
targetWidth = [JXCategoryFactory interpolationFrom:maxWidth to:rightWidth percent:(percent - 0.5)*2];
}
}else if (self.lineStyle == JXCategoryIndicatorLineStyle_LengthenOffset) {
//50%widthx50%xwidth
CGFloat offsetX = self.lineScrollOffsetX;//x
CGFloat maxWidth = rightX - leftX + rightWidth - offsetX*2;
if (percent <= 0.5) {
targetX = [JXCategoryFactory interpolationFrom:leftX to:leftX + offsetX percent:percent*2];;
targetWidth = [JXCategoryFactory interpolationFrom:leftWidth to:maxWidth percent:percent*2];
}else {
targetX = [JXCategoryFactory interpolationFrom:(leftX + offsetX) to:rightX percent:(percent - 0.5)*2];
targetWidth = [JXCategoryFactory interpolationFrom:maxWidth to:rightWidth percent:(percent - 0.5)*2];
}
}
//frame12
if (self.isScrollEnabled == YES || (self.isScrollEnabled == NO && percent == 0)) {
CGRect frame = self.frame;
frame.origin.x = targetX;
frame.size.width = targetWidth;
self.frame = frame;
}
}
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model {
CGRect targetIndicatorFrame = self.frame;
CGFloat targetIndicatorWidth = [self indicatorWidthValue:model.selectedCellFrame];
targetIndicatorFrame.origin.x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - targetIndicatorWidth)/2.0;
targetIndicatorFrame.size.width = targetIndicatorWidth;
if (self.isScrollEnabled) {
if (self.scrollStyle == JXCategoryIndicatorScrollStyleSameAsUserScroll && (model.selectedType == JXCategoryCellSelectedTypeClick | model.selectedType == JXCategoryCellSelectedTypeCode)) {
if (self.animator.isExecuting) {
[self.animator invalid];
self.animator = nil;
}
CGFloat leftX = 0;
CGFloat rightX = 0;
CGFloat leftWidth = 0;
CGFloat rightWidth = 0;
BOOL isNeedReversePercent = NO;
if (self.frame.origin.x > model.selectedCellFrame.origin.x) {
leftWidth = [self indicatorWidthValue:model.selectedCellFrame];
rightWidth = self.frame.size.width;
leftX = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - leftWidth)/2;;
rightX = self.frame.origin.x;
isNeedReversePercent = YES;
}else {
leftWidth = self.frame.size.width;
rightWidth = [self indicatorWidthValue:model.selectedCellFrame];
leftX = self.frame.origin.x;
rightX = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - rightWidth)/2;
}
__weak typeof(self) weakSelf = self;
if (self.lineStyle == JXCategoryIndicatorLineStyle_Normal) {
[UIView animateWithDuration:self.scrollAnimationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.frame = targetIndicatorFrame;
} completion: nil];
}else if (self.lineStyle == JXCategoryIndicatorLineStyle_Lengthen) {
CGFloat maxWidth = rightX - leftX + rightWidth;
//50%width50%xwidth
self.animator = [[JXCategoryViewAnimator alloc] init];
self.animator.progressCallback = ^(CGFloat percent) {
if (isNeedReversePercent) {
percent = 1 - percent;
}
CGFloat targetX = 0;
CGFloat targetWidth = 0;
if (percent <= 0.5) {
targetX = leftX;
targetWidth = [JXCategoryFactory interpolationFrom:leftWidth to:maxWidth percent:percent*2];
}else {
targetX = [JXCategoryFactory interpolationFrom:leftX to:rightX percent:(percent - 0.5)*2];
targetWidth = [JXCategoryFactory interpolationFrom:maxWidth to:rightWidth percent:(percent - 0.5)*2];
}
CGRect toFrame = weakSelf.frame;
toFrame.origin.x = targetX;
toFrame.size.width = targetWidth;
weakSelf.frame = toFrame;
};
[self.animator start];
}else if (self.lineStyle == JXCategoryIndicatorLineStyle_LengthenOffset) {
//50%widthx50%xwidth
CGFloat offsetX = self.lineScrollOffsetX;//x
CGFloat maxWidth = rightX - leftX + rightWidth - offsetX*2;
self.animator = [[JXCategoryViewAnimator alloc] init];
self.animator.progressCallback = ^(CGFloat percent) {
if (isNeedReversePercent) {
percent = 1 - percent;
}
CGFloat targetX = 0;
CGFloat targetWidth = 0;
if (percent <= 0.5) {
targetX = [JXCategoryFactory interpolationFrom:leftX to:leftX + offsetX percent:percent*2];;
targetWidth = [JXCategoryFactory interpolationFrom:leftWidth to:maxWidth percent:percent*2];
}else {
targetX = [JXCategoryFactory interpolationFrom:(leftX + offsetX) to:rightX percent:(percent - 0.5)*2];
targetWidth = [JXCategoryFactory interpolationFrom:maxWidth to:rightWidth percent:(percent - 0.5)*2];
}
CGRect toFrame = weakSelf.frame;
toFrame.origin.x = targetX;
toFrame.size.width = targetWidth;
weakSelf.frame = toFrame;
};
[self.animator start];
}
}else if (self.scrollStyle == JXCategoryIndicatorScrollStyleSimple) {
[UIView animateWithDuration:self.scrollAnimationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.frame = targetIndicatorFrame;
} completion: nil];
}
}else {
self.frame = targetIndicatorFrame;
}
}
@end

View File

@@ -0,0 +1,30 @@
//
// JXCategoryIndicatorRainbowLineView.h
// JXCategoryView
//
// Created by jiaxin on 2018/12/13.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorLineView.h"
NS_ASSUME_NONNULL_BEGIN
/**
彩虹效果的指示器
!!!: 会无视 JXCategoryIndicatorLineView 的 indicatorColor 属性,以 indicatorColors 为准。
*/
@interface JXCategoryIndicatorRainbowLineView : JXCategoryIndicatorLineView
/**
指示器颜色数组
数量需要与 cell 的数量相等。没有提供默认值,必须要赋值该属性。
categoryView 在 reloadData 的时候,也要一并更新该属性,不然会出现数组越界。
*/
@property (nonatomic, strong) NSArray <UIColor *> *indicatorColors;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,38 @@
//
// JXCategoryIndicatorRainbowLineView.m
// JXCategoryView
//
// Created by jiaxin on 2018/12/13.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorRainbowLineView.h"
#import "JXCategoryFactory.h"
@implementation JXCategoryIndicatorRainbowLineView
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model {
[super jx_refreshState:model];
UIColor *color = self.indicatorColors[model.selectedIndex];
self.backgroundColor = color;
}
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model {
[super jx_contentScrollViewDidScroll:model];
UIColor *leftColor = self.indicatorColors[model.leftIndex];
UIColor *rightColor = self.indicatorColors[model.rightIndex];
UIColor *color = [JXCategoryFactory interpolationColorFrom:leftColor to:rightColor percent:model.percent];
self.backgroundColor = color;
}
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model {
[super jx_selectedCell:model];
UIColor *color = self.indicatorColors[model.selectedIndex];
self.backgroundColor = color;
}
@end

View File

@@ -0,0 +1,14 @@
//
// JXCategoryIndicatorTriangleView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorComponentView.h"
/// 三角形样式的指示器
@interface JXCategoryIndicatorTriangleView : JXCategoryIndicatorComponentView
@end

View File

@@ -0,0 +1,109 @@
//
// JXCategoryIndicatorTriangleView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/17.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorTriangleView.h"
#import "JXCategoryFactory.h"
@interface JXCategoryIndicatorTriangleView ()
@property (nonatomic, strong) CAShapeLayer *triangleLayer;
@end
@implementation JXCategoryIndicatorTriangleView
#pragma mark - Initialize
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configureDefaulteValue];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self configureDefaulteValue];
}
return self;
}
- (void)configureDefaulteValue {
self.indicatorWidth = 14;
self.indicatorHeight = 10;
_triangleLayer = [CAShapeLayer layer];
[self.layer addSublayer:self.triangleLayer];
}
#pragma mark - JXCategoryIndicatorProtocol
- (void)jx_refreshState:(JXCategoryIndicatorParamsModel *)model {
CGFloat x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - [self indicatorWidthValue:model.selectedCellFrame])/2;
CGFloat y = self.superview.bounds.size.height - [self indicatorHeightValue:model.selectedCellFrame] - self.verticalMargin;
if (self.componentPosition == JXCategoryComponentPosition_Top) {
y = self.verticalMargin;
}
self.frame = CGRectMake(x, y, [self indicatorWidthValue:model.selectedCellFrame], [self indicatorHeightValue:model.selectedCellFrame]);
[CATransaction begin];
[CATransaction setDisableActions:NO];
self.triangleLayer.fillColor = self.indicatorColor.CGColor;
self.triangleLayer.frame = self.bounds;
UIBezierPath *path = [UIBezierPath bezierPath];
if (self.componentPosition == JXCategoryComponentPosition_Bottom) {
[path moveToPoint:CGPointMake(self.bounds.size.width/2, 0)];
[path addLineToPoint:CGPointMake(0, self.bounds.size.height)];
[path addLineToPoint:CGPointMake(self.bounds.size.width, self.bounds.size.height)];
} else {
[path moveToPoint:CGPointMake(0, 0)];
[path addLineToPoint:CGPointMake(self.bounds.size.width, 0)];
[path addLineToPoint:CGPointMake(self.bounds.size.width/2, self.bounds.size.height)];
}
[path closePath];
self.triangleLayer.path = path.CGPath;
[CATransaction commit];
}
- (void)jx_contentScrollViewDidScroll:(JXCategoryIndicatorParamsModel *)model {
CGRect rightCellFrame = model.rightCellFrame;
CGRect leftCellFrame = model.leftCellFrame;
CGFloat percent = model.percent;
CGFloat targetWidth = [self indicatorWidthValue:model.leftCellFrame];
CGFloat targetX = 0;
if (percent == 0) {
targetX = leftCellFrame.origin.x + (leftCellFrame.size.width - targetWidth)/2.0;
} else {
CGFloat leftX = leftCellFrame.origin.x + (leftCellFrame.size.width - targetWidth)/2;
CGFloat rightX = rightCellFrame.origin.x + (rightCellFrame.size.width - targetWidth)/2;
targetX = [JXCategoryFactory interpolationFrom:leftX to:rightX percent:percent];
}
//frame12
if (self.isScrollEnabled == YES || (self.isScrollEnabled == NO && percent == 0)) {
CGRect frame = self.frame;
frame.origin.x = targetX;
self.frame = frame;
}
}
- (void)jx_selectedCell:(JXCategoryIndicatorParamsModel *)model {
CGRect toFrame = self.frame;
toFrame.origin.x = model.selectedCellFrame.origin.x + (model.selectedCellFrame.size.width - [self indicatorWidthValue:model.selectedCellFrame])/2;
if (self.isScrollEnabled) {
[UIView animateWithDuration:self.scrollAnimationDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
self.frame = toFrame;
} completion:^(BOOL finished) {
}];
} else {
self.frame = toFrame;
}
}
@end

View File

@@ -0,0 +1,13 @@
//
// JXCategoryComponetCell.h
// DQGuess
//
// Created by jiaxin on 2018/7/25.
// Copyright © 2018年 jingbo. All rights reserved.
//
#import "JXCategoryBaseCell.h"
@interface JXCategoryIndicatorCell : JXCategoryBaseCell
@end

View File

@@ -0,0 +1,52 @@
//
// JXCategoryComponetCell.m
// DQGuess
//
// Created by jiaxin on 2018/7/25.
// Copyright © 2018 jingbo. All rights reserved.
//
#import "JXCategoryIndicatorCell.h"
#import "JXCategoryIndicatorCellModel.h"
@interface JXCategoryIndicatorCell ()
@property (nonatomic, strong) UIView *separatorLine;
@end
@implementation JXCategoryIndicatorCell
- (void)initializeViews {
[super initializeViews];
self.separatorLine = [[UIView alloc] init];
self.separatorLine.hidden = YES;
[self.contentView addSubview:self.separatorLine];
}
- (void)layoutSubviews {
[super layoutSubviews];
JXCategoryIndicatorCellModel *model = (JXCategoryIndicatorCellModel *)self.cellModel;
CGFloat lineWidth = model.separatorLineSize.width;
CGFloat lineHeight = model.separatorLineSize.height;
self.separatorLine.frame = CGRectMake(self.bounds.size.width - lineWidth + self.cellModel.cellSpacing/2, (self.bounds.size.height - lineHeight)/2.0, lineWidth, lineHeight);
}
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel {
[super reloadData:cellModel];
JXCategoryIndicatorCellModel *model = (JXCategoryIndicatorCellModel *)cellModel;
self.separatorLine.backgroundColor = model.separatorLineColor;
self.separatorLine.hidden = !model.isSepratorLineShowEnabled;
if (model.isCellBackgroundColorGradientEnabled) {
if (model.isSelected) {
self.contentView.backgroundColor = model.cellBackgroundSelectedColor;
}else {
self.contentView.backgroundColor = model.cellBackgroundUnselectedColor;
}
}
}
@end

View File

@@ -0,0 +1,24 @@
//
// JXCategoryComponentCellModel.h
// DQGuess
//
// Created by jiaxin on 2018/7/25.
// Copyright © 2018年 jingbo. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "JXCategoryBaseCellModel.h"
@interface JXCategoryIndicatorCellModel : JXCategoryBaseCellModel
@property (nonatomic, assign, getter=isSepratorLineShowEnabled) BOOL sepratorLineShowEnabled;
@property (nonatomic, strong) UIColor *separatorLineColor;
@property (nonatomic, assign) CGSize separatorLineSize;
@property (nonatomic, assign) CGRect backgroundViewMaskFrame; // 底部指示器的 frame 转换到 cell 的 frame
@property (nonatomic, assign, getter=isCellBackgroundColorGradientEnabled) BOOL cellBackgroundColorGradientEnabled;
@property (nonatomic, strong) UIColor *cellBackgroundSelectedColor;
@property (nonatomic, strong) UIColor *cellBackgroundUnselectedColor;
@end

View File

@@ -0,0 +1,13 @@
//
// JXCategoryComponentCellModel.m
// DQGuess
//
// Created by jiaxin on 2018/7/25.
// Copyright © 2018 jingbo. All rights reserved.
//
#import "JXCategoryIndicatorCellModel.h"
@implementation JXCategoryIndicatorCellModel
@end

View File

@@ -0,0 +1,48 @@
//
// JXCategoryComponentView.h
// DQGuess
//
// Created by jiaxin on 2018/7/25.
// Copyright © 2018年 jingbo. All rights reserved.
//
#import "JXCategoryBaseView.h"
#import "JXCategoryIndicatorCell.h"
#import "JXCategoryIndicatorCellModel.h"
#import "JXCategoryIndicatorProtocol.h"
@interface JXCategoryIndicatorView : JXCategoryBaseView
@property (nonatomic, strong) NSArray <UIView<JXCategoryIndicatorProtocol> *> *indicators;
//----------------------ellBackgroundColor-----------------------//
//cell的背景色是否渐变。默认NO
@property (nonatomic, assign, getter=isCellBackgroundColorGradientEnabled) BOOL cellBackgroundColorGradientEnabled;
//cell普通状态的背景色。默认[UIColor clearColor]
@property (nonatomic, strong) UIColor *cellBackgroundUnselectedColor;
//cell选中状态的背景色。默认[UIColor grayColor]
@property (nonatomic, strong) UIColor *cellBackgroundSelectedColor;
//----------------------separatorLine-----------------------//
//是否显示分割线。默认为NO
@property (nonatomic, assign, getter=isSeparatorLineShowEnabled) BOOL separatorLineShowEnabled;
//分割线颜色。默认为[UIColor lightGrayColor]
@property (nonatomic, strong) UIColor *separatorLineColor;
//分割线的size。默认为CGSizeMake(1/[UIScreen mainScreen].scale, 20)
@property (nonatomic, assign) CGSize separatorLineSize;
@end
@interface JXCategoryIndicatorView (UISubclassingIndicatorHooks)
/**
当contentScrollView滚动时候处理跟随手势的过渡效果。
根据cellModel的左右位置、是否选中、ratio进行过滤数据计算。
@param leftCellModel 左边的cellModel
@param rightCellModel 右边的cellModel
@param ratio 从左往右方向计算的百分比
*/
- (void)refreshLeftCellModel:(JXCategoryBaseCellModel *)leftCellModel rightCellModel:(JXCategoryBaseCellModel *)rightCellModel ratio:(CGFloat)ratio NS_REQUIRES_SUPER;
@end

View File

@@ -0,0 +1,209 @@
//
// JXCategoryIndicatorView.m
// DQGuess
//
// Created by jiaxin on 2018/7/25.
// Copyright © 2018 jingbo. All rights reserved.
//
#import "JXCategoryIndicatorView.h"
#import "JXCategoryIndicatorBackgroundView.h"
#import "JXCategoryFactory.h"
@interface JXCategoryIndicatorView()
@end
@implementation JXCategoryIndicatorView
- (void)initializeData {
[super initializeData];
_separatorLineShowEnabled = NO;
_separatorLineColor = [UIColor lightGrayColor];
_separatorLineSize = CGSizeMake(1/[UIScreen mainScreen].scale, 20);
_cellBackgroundColorGradientEnabled = NO;
_cellBackgroundUnselectedColor = [UIColor whiteColor];
_cellBackgroundSelectedColor = [UIColor lightGrayColor];
}
- (void)initializeViews {
[super initializeViews];
}
- (void)setIndicators:(NSArray<UIView<JXCategoryIndicatorProtocol> *> *)indicators {
_indicators = indicators;
self.collectionView.indicators = indicators;
}
- (void)refreshState {
[super refreshState];
CGRect selectedCellFrame = CGRectZero;
JXCategoryIndicatorCellModel *selectedCellModel;
for (int i = 0; i < self.dataSource.count; i++) {
JXCategoryIndicatorCellModel *cellModel = (JXCategoryIndicatorCellModel *)self.dataSource[i];
cellModel.sepratorLineShowEnabled = self.isSeparatorLineShowEnabled;
cellModel.separatorLineColor = self.separatorLineColor;
cellModel.separatorLineSize = self.separatorLineSize;
cellModel.backgroundViewMaskFrame = CGRectZero;
cellModel.cellBackgroundColorGradientEnabled = self.isCellBackgroundColorGradientEnabled;
cellModel.cellBackgroundSelectedColor = self.cellBackgroundSelectedColor;
cellModel.cellBackgroundUnselectedColor = self.cellBackgroundUnselectedColor;
if (i == self.dataSource.count - 1) {
cellModel.sepratorLineShowEnabled = NO;
}
if (i == self.selectedIndex) {
selectedCellModel = cellModel;
selectedCellFrame = [self getTargetCellFrame:i];
}
}
for (UIView<JXCategoryIndicatorProtocol> *indicator in self.indicators) {
if (self.dataSource.count <= 0) {
indicator.hidden = YES;
} else {
indicator.hidden = NO;
JXCategoryIndicatorParamsModel *indicatorParamsModel = [[JXCategoryIndicatorParamsModel alloc] init];
indicatorParamsModel.selectedIndex = self.selectedIndex;
indicatorParamsModel.selectedCellFrame = selectedCellFrame;
[indicator jx_refreshState:indicatorParamsModel];
if ([indicator isKindOfClass:[JXCategoryIndicatorBackgroundView class]]) {
CGRect maskFrame = indicator.frame;
maskFrame.origin.x = maskFrame.origin.x - selectedCellFrame.origin.x;
selectedCellModel.backgroundViewMaskFrame = maskFrame;
}
}
}
}
- (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel {
[super refreshSelectedCellModel:selectedCellModel unselectedCellModel:unselectedCellModel];
JXCategoryIndicatorCellModel *myUnselectedCellModel = (JXCategoryIndicatorCellModel *)unselectedCellModel;
myUnselectedCellModel.backgroundViewMaskFrame = CGRectZero;
myUnselectedCellModel.cellBackgroundUnselectedColor = self.cellBackgroundUnselectedColor;
myUnselectedCellModel.cellBackgroundSelectedColor = self.cellBackgroundSelectedColor;
JXCategoryIndicatorCellModel *myselectedCellModel = (JXCategoryIndicatorCellModel *)selectedCellModel;
myselectedCellModel.cellBackgroundUnselectedColor = self.cellBackgroundUnselectedColor;
myselectedCellModel.cellBackgroundSelectedColor = self.cellBackgroundSelectedColor;
}
- (void)contentOffsetOfContentScrollViewDidChanged:(CGPoint)contentOffset {
[super contentOffsetOfContentScrollViewDidChanged:contentOffset];
CGFloat ratio = contentOffset.x/self.contentScrollView.bounds.size.width;
if (ratio > self.dataSource.count - 1 || ratio < 0) {
//
return;
}
ratio = MAX(0, MIN(self.dataSource.count - 1, ratio));
NSInteger baseIndex = floorf(ratio);
if (baseIndex + 1 >= self.dataSource.count) {
//
return;
}
CGFloat remainderRatio = ratio - baseIndex;
CGRect leftCellFrame = [self getTargetCellFrame:baseIndex];
CGRect rightCellFrame = [self getTargetCellFrame:baseIndex + 1];
JXCategoryIndicatorParamsModel *indicatorParamsModel = [[JXCategoryIndicatorParamsModel alloc] init];
indicatorParamsModel.selectedIndex = self.selectedIndex;
indicatorParamsModel.leftIndex = baseIndex;
indicatorParamsModel.leftCellFrame = leftCellFrame;
indicatorParamsModel.rightIndex = baseIndex + 1;
indicatorParamsModel.rightCellFrame = rightCellFrame;
indicatorParamsModel.percent = remainderRatio;
if (remainderRatio == 0) {
for (UIView<JXCategoryIndicatorProtocol> *indicator in self.indicators) {
[indicator jx_contentScrollViewDidScroll:indicatorParamsModel];
}
} else {
JXCategoryIndicatorCellModel *leftCellModel = (JXCategoryIndicatorCellModel *)self.dataSource[baseIndex];
leftCellModel.selectedType = JXCategoryCellSelectedTypeUnknown;
JXCategoryIndicatorCellModel *rightCellModel = (JXCategoryIndicatorCellModel *)self.dataSource[baseIndex + 1];
rightCellModel.selectedType = JXCategoryCellSelectedTypeUnknown;
[self refreshLeftCellModel:leftCellModel rightCellModel:rightCellModel ratio:remainderRatio];
for (UIView<JXCategoryIndicatorProtocol> *indicator in self.indicators) {
[indicator jx_contentScrollViewDidScroll:indicatorParamsModel];
if ([indicator isKindOfClass:[JXCategoryIndicatorBackgroundView class]]) {
CGRect leftMaskFrame = indicator.frame;
leftMaskFrame.origin.x = leftMaskFrame.origin.x - leftCellFrame.origin.x;
leftCellModel.backgroundViewMaskFrame = leftMaskFrame;
CGRect rightMaskFrame = indicator.frame;
rightMaskFrame.origin.x = rightMaskFrame.origin.x - rightCellFrame.origin.x;
rightCellModel.backgroundViewMaskFrame = rightMaskFrame;
}
}
JXCategoryBaseCell *leftCell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:baseIndex inSection:0]];
[leftCell reloadData:leftCellModel];
JXCategoryBaseCell *rightCell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:baseIndex + 1 inSection:0]];
[rightCell reloadData:rightCellModel];
}
}
- (BOOL)selectCellAtIndex:(NSInteger)index selectedType:(JXCategoryCellSelectedType)selectedType {
NSInteger lastSelectedIndex = self.selectedIndex;
BOOL result = [super selectCellAtIndex:index selectedType:selectedType];
if (!result) {
return NO;
}
CGRect clickedCellFrame = [self getTargetSelectedCellFrame:index selectedType:selectedType];
JXCategoryIndicatorCellModel *selectedCellModel = (JXCategoryIndicatorCellModel *)self.dataSource[index];
selectedCellModel.selectedType = selectedType;
for (UIView<JXCategoryIndicatorProtocol> *indicator in self.indicators) {
JXCategoryIndicatorParamsModel *indicatorParamsModel = [[JXCategoryIndicatorParamsModel alloc] init];
indicatorParamsModel.lastSelectedIndex = lastSelectedIndex;
indicatorParamsModel.selectedIndex = index;
indicatorParamsModel.selectedCellFrame = clickedCellFrame;
indicatorParamsModel.selectedType = selectedType;
[indicator jx_selectedCell:indicatorParamsModel];
if ([indicator isKindOfClass:[JXCategoryIndicatorBackgroundView class]]) {
CGRect maskFrame = indicator.frame;
maskFrame.origin.x = maskFrame.origin.x - clickedCellFrame.origin.x;
selectedCellModel.backgroundViewMaskFrame = maskFrame;
}
}
JXCategoryIndicatorCell *selectedCell = (JXCategoryIndicatorCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[selectedCell reloadData:selectedCellModel];
return YES;
}
@end
@implementation JXCategoryIndicatorView (UISubclassingIndicatorHooks)
- (void)refreshLeftCellModel:(JXCategoryBaseCellModel *)leftCellModel rightCellModel:(JXCategoryBaseCellModel *)rightCellModel ratio:(CGFloat)ratio {
if (self.isCellBackgroundColorGradientEnabled) {
//cell
JXCategoryIndicatorCellModel *leftModel = (JXCategoryIndicatorCellModel *)leftCellModel;
JXCategoryIndicatorCellModel *rightModel = (JXCategoryIndicatorCellModel *)rightCellModel;
if (leftModel.isSelected) {
leftModel.cellBackgroundSelectedColor = [JXCategoryFactory interpolationColorFrom:self.cellBackgroundSelectedColor to:self.cellBackgroundUnselectedColor percent:ratio];
leftModel.cellBackgroundUnselectedColor = self.cellBackgroundUnselectedColor;
}else {
leftModel.cellBackgroundUnselectedColor = [JXCategoryFactory interpolationColorFrom:self.cellBackgroundSelectedColor to:self.cellBackgroundUnselectedColor percent:ratio];
leftModel.cellBackgroundSelectedColor = self.cellBackgroundSelectedColor;
}
if (rightModel.isSelected) {
rightModel.cellBackgroundSelectedColor = [JXCategoryFactory interpolationColorFrom:self.cellBackgroundUnselectedColor to:self.cellBackgroundSelectedColor percent:ratio];
rightModel.cellBackgroundUnselectedColor = self.cellBackgroundUnselectedColor;
}else {
rightModel.cellBackgroundUnselectedColor = [JXCategoryFactory interpolationColorFrom:self.cellBackgroundUnselectedColor to:self.cellBackgroundSelectedColor percent:ratio];
rightModel.cellBackgroundSelectedColor = self.cellBackgroundSelectedColor;
}
}
}
@end

View File

@@ -0,0 +1,23 @@
#import "JXCategoryBaseView.h"
#import "JXCategoryIndicatorView.h"
#import "JXCategoryTitleView.h"
#import "JXCategoryImageView.h"
#import "JXCategoryTitleImageView.h"
#import "JXCategoryNumberView.h"
#import "JXCategoryDotView.h"
#import "JXCategoryFactory.h"
#import "JXCategoryIndicatorProtocol.h"
#import "JXCategoryViewDefines.h"
#import "JXCategoryListContainerView.h"
#import "JXCategoryIndicatorComponentView.h"
#import "JXCategoryIndicatorLineView.h"
#import "JXCategoryIndicatorTriangleView.h"
#import "JXCategoryIndicatorImageView.h"
#import "JXCategoryIndicatorBackgroundView.h"
#import "JXCategoryIndicatorBallView.h"
#import "JXCategoryIndicatorRainbowLineView.h"

View File

@@ -0,0 +1,13 @@
//
// JXCategoryNumberCell.h
// DQGuess
//
// Created by jiaxin on 2018/4/9.
// Copyright © 2018年 jingbo. All rights reserved.
//
#import "JXCategoryTitleCell.h"
@interface JXCategoryNumberCell : JXCategoryTitleCell
@property (nonatomic, strong) UILabel *numberLabel;
@end

View File

@@ -0,0 +1,62 @@
//
// JXCategoryNumberCell.m
// DQGuess
//
// Created by jiaxin on 2018/4/9.
// Copyright © 2018 jingbo. All rights reserved.
//
#import "JXCategoryNumberCell.h"
#import "JXCategoryNumberCellModel.h"
@interface JXCategoryNumberCell ()
@property (nonatomic, strong) NSLayoutConstraint *numberCenterXConstraint;
@property (nonatomic, strong) NSLayoutConstraint *numberCenterYConstraint;
@property (nonatomic, strong) NSLayoutConstraint *numberHeightConstraint;
@property (nonatomic, strong) NSLayoutConstraint *numberWidthConstraint;
@end
@implementation JXCategoryNumberCell
- (void)prepareForReuse {
[super prepareForReuse];
self.numberLabel.text = nil;
}
- (void)initializeViews {
[super initializeViews];
self.numberLabel = [[UILabel alloc] init];
self.numberLabel.textAlignment = NSTextAlignmentCenter;
self.numberLabel.layer.masksToBounds = YES;
[self.contentView addSubview:self.numberLabel];
self.numberLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.numberCenterXConstraint = [self.numberLabel.centerXAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor];
self.numberCenterYConstraint = [self.numberLabel.centerYAnchor constraintEqualToAnchor:self.titleLabel.topAnchor];
self.numberHeightConstraint = [self.numberLabel.heightAnchor constraintEqualToConstant:0];
self.numberWidthConstraint = [self.numberLabel.widthAnchor constraintEqualToConstant:0];
[NSLayoutConstraint activateConstraints:@[self.numberCenterXConstraint, self.numberCenterYConstraint, self.numberWidthConstraint, self.numberHeightConstraint]];
}
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel {
[super reloadData:cellModel];
JXCategoryNumberCellModel *myCellModel = (JXCategoryNumberCellModel *)cellModel;
self.numberLabel.hidden = (myCellModel.count == 0);
self.numberLabel.backgroundColor = myCellModel.numberBackgroundColor;
self.numberLabel.font = myCellModel.numberLabelFont;
self.numberLabel.textColor = myCellModel.numberTitleColor;
self.numberLabel.text = myCellModel.numberString;
self.numberLabel.layer.cornerRadius = myCellModel.numberLabelHeight/2.0;
self.numberHeightConstraint.constant = myCellModel.numberLabelHeight;
self.numberCenterXConstraint.constant = myCellModel.numberLabelOffset.x;
self.numberCenterYConstraint.constant = myCellModel.numberLabelOffset.y;
if (myCellModel.count < 10 && myCellModel.shouldMakeRoundWhenSingleNumber) {
self.numberWidthConstraint.constant = myCellModel.numberLabelHeight;
}else {
self.numberWidthConstraint.constant = myCellModel.numberStringWidth + myCellModel.numberLabelWidthIncrement;
}
}
@end

View File

@@ -0,0 +1,25 @@
//
// JXCategoryNumberCellModel.h
// DQGuess
//
// Created by jiaxin on 2018/4/24.
// Copyright © 2018年 jingbo. All rights reserved.
//
#import "JXCategoryTitleCellModel.h"
@interface JXCategoryNumberCellModel : JXCategoryTitleCellModel
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, copy) NSString *numberString;
@property (nonatomic, assign, readonly) CGFloat numberStringWidth;
@property (nonatomic, copy) void(^numberStringFormatterBlock)(NSInteger number);
@property (nonatomic, strong) UIColor *numberBackgroundColor;
@property (nonatomic, strong) UIColor *numberTitleColor;
@property (nonatomic, assign) CGFloat numberLabelWidthIncrement;
@property (nonatomic, assign) CGFloat numberLabelHeight;
@property (nonatomic, strong) UIFont *numberLabelFont;
@property (nonatomic, assign) CGPoint numberLabelOffset;
@property (nonatomic, assign) BOOL shouldMakeRoundWhenSingleNumber;
@end

View File

@@ -0,0 +1,37 @@
//
// JXCategoryNumberCellModel.m
// DQGuess
//
// Created by jiaxin on 2018/4/24.
// Copyright © 2018 jingbo. All rights reserved.
//
#import "JXCategoryNumberCellModel.h"
@implementation JXCategoryNumberCellModel
- (void)setNumberString:(NSString *)numberString {
_numberString = numberString;
[self updateNumberSizeWidthIfNeeded];
}
- (void)setNumberLabelHeight:(CGFloat)numberLabelHeight {
_numberLabelHeight = numberLabelHeight;
[self updateNumberSizeWidthIfNeeded];
}
- (void)setNumberLabelFont:(UIFont *)numberLabelFont {
_numberLabelFont = numberLabelFont;
[self updateNumberSizeWidthIfNeeded];
}
- (void)updateNumberSizeWidthIfNeeded {
if (self.numberLabelFont != nil) {
_numberStringWidth = [self.numberString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, self.numberLabelHeight) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : self.numberLabelFont} context:nil].size.width;
}
}
@end

View File

@@ -0,0 +1,60 @@
//
// JXCategoryNumberView.h
// DQGuess
//
// Created by jiaxin on 2018/4/9.
// Copyright © 2018年 jingbo. All rights reserved.
//
#import "JXCategoryTitleView.h"
#import "JXCategoryNumberCell.h"
#import "JXCategoryNumberCellModel.h"
@interface JXCategoryNumberView : JXCategoryTitleView
/**
需要与titles的count对应
*/
@property (nonatomic, strong) NSArray <NSNumber *> *counts;
/**
内部默认不会格式化数字直接转成字符串显示。比如业务需要数字超过999显示999+可以通过该block实现。
*/
@property (nonatomic, copy) NSString *(^numberStringFormatterBlock)(NSInteger number);
/**
numberLabel的font默认[UIFont systemFontOfSize:11]
*/
@property (nonatomic, strong) UIFont *numberLabelFont;
/**
数字的背景色,默认:[UIColor colorWithRed:241/255.0 green:147/255.0 blue:95/255.0 alpha:1]
*/
@property (nonatomic, strong) UIColor *numberBackgroundColor;
/**
数字的title颜色默认[UIColor whiteColor]
*/
@property (nonatomic, strong) UIColor *numberTitleColor;
/**
numberLabel的宽度补偿label真实的宽度是文字内容的宽度加上补偿的宽度默认10
*/
@property (nonatomic, assign) CGFloat numberLabelWidthIncrement;
/**
numberLabel的高度默认14
*/
@property (nonatomic, assign) CGFloat numberLabelHeight;
/**
numberLabel x,y方向的偏移 +值:水平方向向右,竖直方向向下)
*/
@property (nonatomic, assign) CGPoint numberLabelOffset;
/**
当是单一数字时是否让numberLabel变成圆。即numberLabel的宽度等于高度cornerRadius等于高度/2。当为true单一数字时会忽略numberLabelWidthIncrement属性。默认为NO
*/
@property (nonatomic, assign) BOOL shouldMakeRoundWhenSingleNumber;
@end

View File

@@ -0,0 +1,61 @@
//
// JXCategoryNumberView.m
// DQGuess
//
// Created by jiaxin on 2018/4/9.
// Copyright © 2018 jingbo. All rights reserved.
//
#import "JXCategoryNumberView.h"
@implementation JXCategoryNumberView
- (void)dealloc {
self.numberStringFormatterBlock = nil;
}
- (void)initializeData {
[super initializeData];
self.cellSpacing = 25;
_numberTitleColor = [UIColor whiteColor];
_numberBackgroundColor = [UIColor colorWithRed:241/255.0 green:147/255.0 blue:95/255.0 alpha:1];
_numberLabelHeight = 14;
_numberLabelWidthIncrement = 10;
_numberLabelFont = [UIFont systemFontOfSize:11];
_shouldMakeRoundWhenSingleNumber = NO;
}
- (Class)preferredCellClass {
return [JXCategoryNumberCell class];
}
- (void)refreshDataSource {
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:self.titles.count];
for (int i = 0; i < self.titles.count; i++) {
JXCategoryNumberCellModel *cellModel = [[JXCategoryNumberCellModel alloc] init];
[tempArray addObject:cellModel];
}
self.dataSource = [NSArray arrayWithArray:tempArray];
}
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index {
[super refreshCellModel:cellModel index:index];
JXCategoryNumberCellModel *myCellModel = (JXCategoryNumberCellModel *)cellModel;
myCellModel.count = [self.counts[index] integerValue];
if (self.numberStringFormatterBlock != nil) {
myCellModel.numberString = self.numberStringFormatterBlock(myCellModel.count);
}else {
myCellModel.numberString = [NSString stringWithFormat:@"%ld", (long)myCellModel.count];
}
myCellModel.numberBackgroundColor = self.numberBackgroundColor;
myCellModel.numberTitleColor = self.numberTitleColor;
myCellModel.numberLabelHeight = self.numberLabelHeight;
myCellModel.numberLabelOffset = self.numberLabelOffset;
myCellModel.numberLabelWidthIncrement = self.numberLabelWidthIncrement;
myCellModel.numberLabelFont = self.numberLabelFont;
myCellModel.shouldMakeRoundWhenSingleNumber = self.shouldMakeRoundWhenSingleNumber;
}
@end

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,20 @@
//
// RTLManager.h
// JXCategoryView
//
// Created by jiaxin on 2020/7/3.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RTLManager : NSObject
+ (BOOL)supportRTL;
+ (void)horizontalFlipView:(UIView *)view;
+ (void)horizontalFlipViewIfNeeded:(UIView *)view;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,26 @@
//
// RTLManager.m
// JXCategoryView
//
// Created by jiaxin on 2020/7/3.
//
#import "RTLManager.h"
@implementation RTLManager
+ (BOOL)supportRTL {
return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:UIView.appearance.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft;
}
+ (void)horizontalFlipView:(UIView *)view {
view.transform = CGAffineTransformMakeScale(-1, 1);
}
+ (void)horizontalFlipViewIfNeeded:(UIView *)view {
if ([self supportRTL]) {
[self horizontalFlipView:view];
}
}
@end

View File

@@ -0,0 +1,27 @@
//
// JXCategoryTitleCell.h
// UI系列测试
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorCell.h"
#import "JXCategoryViewDefines.h"
@class JXCategoryTitleCellModel;
@interface JXCategoryTitleCell : JXCategoryIndicatorCell
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *maskTitleLabel;
@property (nonatomic, strong) NSLayoutConstraint *titleLabelCenterX;
@property (nonatomic, strong) NSLayoutConstraint *titleLabelCenterY;
@property (nonatomic, strong) NSLayoutConstraint *maskTitleLabelCenterX;
- (JXCategoryCellSelectedAnimationBlock)preferredTitleZoomAnimationBlock:(JXCategoryTitleCellModel *)cellModel baseScale:(CGFloat)baseScale;
- (JXCategoryCellSelectedAnimationBlock)preferredTitleStrokeWidthAnimationBlock:(JXCategoryTitleCellModel *)cellModel attributedString:(NSMutableAttributedString *)attributedString;
- (JXCategoryCellSelectedAnimationBlock)preferredTitleColorAnimationBlock:(JXCategoryTitleCellModel *)cellModel;
@end

View File

@@ -0,0 +1,240 @@
//
// JXCategoryTitleCell.m
// UI
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryTitleCell.h"
#import "JXCategoryTitleCellModel.h"
#import "JXCategoryFactory.h"
#import "RTLManager.h"
@interface JXCategoryTitleCell ()
@property (nonatomic, strong) CALayer *titleMaskLayer;
@property (nonatomic, strong) CALayer *maskTitleMaskLayer;
@property (nonatomic, strong) NSLayoutConstraint *maskTitleLabelCenterY;
@end
@implementation JXCategoryTitleCell
- (void)initializeViews {
[super initializeViews];
self.isAccessibilityElement = true;
self.accessibilityTraits = UIAccessibilityTraitButton;
_titleLabel = [[UILabel alloc] init];
self.titleLabel.clipsToBounds = YES;
self.titleLabel.textAlignment = NSTextAlignmentCenter;
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.titleLabel];
self.titleLabelCenterX = [self.titleLabel.centerXAnchor constraintEqualToAnchor:self.contentView.centerXAnchor constant:0];
self.titleLabelCenterY = [self.titleLabel.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor constant:0];
_titleMaskLayer = [CALayer layer];
self.titleMaskLayer.backgroundColor = [UIColor redColor].CGColor;
_maskTitleLabel = [[UILabel alloc] init];
self.maskTitleLabel.hidden = YES;
self.maskTitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.maskTitleLabel.textAlignment = NSTextAlignmentCenter;
[self.contentView addSubview:self.maskTitleLabel];
self.maskTitleLabelCenterX = [self.maskTitleLabel.centerXAnchor constraintEqualToAnchor:self.contentView.centerXAnchor];
self.maskTitleLabelCenterY = [self.maskTitleLabel.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor];
_maskTitleMaskLayer = [CALayer layer];
self.maskTitleMaskLayer.backgroundColor = [UIColor redColor].CGColor;
self.maskTitleLabel.layer.mask = self.maskTitleMaskLayer;
[NSLayoutConstraint activateConstraints:@[self.titleLabelCenterX, self.titleLabelCenterY, self.maskTitleLabelCenterX, self.maskTitleLabelCenterY]];
}
- (void)layoutSubviews {
[super layoutSubviews];
JXCategoryTitleCellModel *myCellModel = (JXCategoryTitleCellModel *)self.cellModel;
switch (myCellModel.titleLabelAnchorPointStyle) {
case JXCategoryTitleLabelAnchorPointStyleCenter: {
self.titleLabel.layer.anchorPoint = CGPointMake(0.5, 0.5);
self.maskTitleLabel.layer.anchorPoint = CGPointMake(0.5, 0.5);
self.titleLabelCenterY.constant = 0 + myCellModel.titleLabelVerticalOffset;
break;
}
case JXCategoryTitleLabelAnchorPointStyleTop: {
self.titleLabel.layer.anchorPoint = CGPointMake(0.5, 0);
self.maskTitleLabel.layer.anchorPoint = CGPointMake(0.5, 0);
CGFloat percent = (myCellModel.titleLabelCurrentZoomScale - myCellModel.titleLabelNormalZoomScale)/(myCellModel.titleLabelSelectedZoomScale - myCellModel.titleLabelNormalZoomScale);
self.titleLabelCenterY.constant = -myCellModel.titleHeight/2 - myCellModel.titleLabelVerticalOffset - myCellModel.titleLabelZoomSelectedVerticalOffset*percent;
break;
}
case JXCategoryTitleLabelAnchorPointStyleBottom: {
self.titleLabel.layer.anchorPoint = CGPointMake(0.5, 1);
self.maskTitleLabel.layer.anchorPoint = CGPointMake(0.5, 1);
CGFloat percent = (myCellModel.titleLabelCurrentZoomScale - myCellModel.titleLabelNormalZoomScale)/(myCellModel.titleLabelSelectedZoomScale - myCellModel.titleLabelNormalZoomScale);
self.titleLabelCenterY.constant = myCellModel.titleHeight/2 + myCellModel.titleLabelVerticalOffset + myCellModel.titleLabelZoomSelectedVerticalOffset*percent;
break;
}
}
}
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel {
[super reloadData:cellModel];
JXCategoryTitleCellModel *myCellModel = (JXCategoryTitleCellModel *)cellModel;
self.accessibilityLabel = myCellModel.title;
self.titleLabel.numberOfLines = myCellModel.titleNumberOfLines;
self.maskTitleLabel.numberOfLines = myCellModel.titleNumberOfLines;
if (myCellModel.isTitleLabelZoomEnabled) {
//fonttitleLabelZoomScaletransform
UIFont *maxScaleFont = [UIFont fontWithDescriptor:myCellModel.titleFont.fontDescriptor size:myCellModel.titleFont.pointSize*myCellModel.titleLabelSelectedZoomScale];
CGFloat baseScale = myCellModel.titleFont.lineHeight/maxScaleFont.lineHeight;
if (myCellModel.isSelectedAnimationEnabled && [self checkCanStartSelectedAnimation:myCellModel]) {
JXCategoryCellSelectedAnimationBlock block = [self preferredTitleZoomAnimationBlock:myCellModel baseScale:baseScale];
[self addSelectedAnimationBlock:block];
} else {
self.titleLabel.font = maxScaleFont;
self.maskTitleLabel.font = maxScaleFont;
CGAffineTransform currentTransform = CGAffineTransformMakeScale(baseScale*myCellModel.titleLabelCurrentZoomScale, baseScale*myCellModel.titleLabelCurrentZoomScale);
self.titleLabel.transform = currentTransform;
self.maskTitleLabel.transform = currentTransform;
}
} else {
if (myCellModel.isSelected) {
self.titleLabel.font = myCellModel.titleSelectedFont;
self.maskTitleLabel.font = myCellModel.titleSelectedFont;
} else {
self.titleLabel.font = myCellModel.titleFont;
self.maskTitleLabel.font = myCellModel.titleFont;
}
}
NSString *titleString = myCellModel.title ? myCellModel.title : @"";
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:titleString];
if (myCellModel.isTitleLabelStrokeWidthEnabled) {
if (myCellModel.isSelectedAnimationEnabled && [self checkCanStartSelectedAnimation:myCellModel]) {
JXCategoryCellSelectedAnimationBlock block = [self preferredTitleStrokeWidthAnimationBlock:myCellModel attributedString:attributedString];
[self addSelectedAnimationBlock:block];
} else {
[attributedString addAttribute:NSStrokeWidthAttributeName value:@(myCellModel.titleLabelCurrentStrokeWidth) range:NSMakeRange(0, titleString.length)];
self.titleLabel.attributedText = attributedString;
self.maskTitleLabel.attributedText = attributedString;
}
} else {
self.titleLabel.attributedText = attributedString;
self.maskTitleLabel.attributedText = attributedString;
}
if (myCellModel.isTitleLabelMaskEnabled) {
self.maskTitleLabel.hidden = NO;
self.titleLabel.textColor = myCellModel.titleNormalColor;
self.maskTitleLabel.textColor = myCellModel.titleSelectedColor;
[self.contentView setNeedsLayout];
[self.contentView layoutIfNeeded];
CGRect topMaskframe = myCellModel.backgroundViewMaskFrame;
//cellbackgroundViewMaskFramemaskTitleLabel
//使self.bounds.size.widthself.contentView.bounds.size.widthself.boundsself.contentView.bounds
topMaskframe.origin.y = 0;
CGRect bottomMaskFrame = topMaskframe;
CGFloat maskStartX = 0;
if (self.maskTitleLabel.bounds.size.width >= self.bounds.size.width) {
topMaskframe.origin.x -= (self.maskTitleLabel.bounds.size.width -self.bounds.size.width)/2;
bottomMaskFrame.size.width = self.maskTitleLabel.bounds.size.width;
maskStartX = -(self.maskTitleLabel.bounds.size.width -self.bounds.size.width)/2;
} else {
bottomMaskFrame.size.width = self.bounds.size.width;
topMaskframe.origin.x -= (self.bounds.size.width -self.maskTitleLabel.bounds.size.width)/2;
maskStartX = 0;
}
bottomMaskFrame.origin.x = topMaskframe.origin.x;
if (topMaskframe.origin.x > maskStartX) {
bottomMaskFrame.origin.x = topMaskframe.origin.x - bottomMaskFrame.size.width;
} else {
bottomMaskFrame.origin.x = CGRectGetMaxX(topMaskframe);
}
// RTLx
if ([RTLManager supportRTL]) {
topMaskframe.origin.x = self.maskTitleMaskLayer.superlayer.frame.size.width - CGRectGetMaxX(topMaskframe);
bottomMaskFrame.origin.x = self.titleMaskLayer.superlayer.frame.size.width - CGRectGetMaxX(bottomMaskFrame);
}
[CATransaction begin];
[CATransaction setDisableActions:YES];
if (topMaskframe.size.width > 0 && CGRectIntersectsRect(topMaskframe, self.maskTitleLabel.frame)) {
self.titleLabel.layer.mask = self.titleMaskLayer;
self.maskTitleMaskLayer.frame = topMaskframe;
self.titleMaskLayer.frame = bottomMaskFrame;
} else {
self.maskTitleMaskLayer.frame = topMaskframe;
self.titleLabel.layer.mask = nil;
}
[CATransaction commit];
} else {
self.maskTitleLabel.hidden = YES;
self.titleLabel.layer.mask = nil;
if (myCellModel.isSelectedAnimationEnabled && [self checkCanStartSelectedAnimation:myCellModel]) {
JXCategoryCellSelectedAnimationBlock block = [self preferredTitleColorAnimationBlock:myCellModel];
[self addSelectedAnimationBlock:block];
} else {
self.titleLabel.textColor = myCellModel.titleCurrentColor;
}
}
[self startSelectedAnimationIfNeeded:myCellModel];
}
#pragma mark - Public
- (JXCategoryCellSelectedAnimationBlock)preferredTitleZoomAnimationBlock:(JXCategoryTitleCellModel *)cellModel baseScale:(CGFloat)baseScale {
__weak typeof(self) weakSelf = self;
return ^(CGFloat percent) {
if (cellModel.isSelected) {
//scale
cellModel.titleLabelCurrentZoomScale = [JXCategoryFactory interpolationFrom:cellModel.titleLabelNormalZoomScale to:cellModel.titleLabelSelectedZoomScale percent:percent];
} else {
//scale
cellModel.titleLabelCurrentZoomScale = [JXCategoryFactory interpolationFrom:cellModel.titleLabelSelectedZoomScale to:cellModel.titleLabelNormalZoomScale percent:percent];
}
CGAffineTransform currentTransform = CGAffineTransformMakeScale(baseScale*cellModel.titleLabelCurrentZoomScale, baseScale*cellModel.titleLabelCurrentZoomScale);
weakSelf.titleLabel.transform = currentTransform;
weakSelf.maskTitleLabel.transform = currentTransform;
};
}
- (JXCategoryCellSelectedAnimationBlock)preferredTitleStrokeWidthAnimationBlock:(JXCategoryTitleCellModel *)cellModel attributedString:(NSMutableAttributedString *)attributedString {
__weak typeof(self) weakSelf = self;
return ^(CGFloat percent) {
if (cellModel.isSelected) {
//StrokeWidth
cellModel.titleLabelCurrentStrokeWidth = [JXCategoryFactory interpolationFrom:cellModel.titleLabelNormalStrokeWidth to:cellModel.titleLabelSelectedStrokeWidth percent:percent];
} else {
//StrokeWidth
cellModel.titleLabelCurrentStrokeWidth = [JXCategoryFactory interpolationFrom:cellModel.titleLabelSelectedStrokeWidth to:cellModel.titleLabelNormalStrokeWidth percent:percent];
}
[attributedString addAttribute:NSStrokeWidthAttributeName value:@(cellModel.titleLabelCurrentStrokeWidth) range:NSMakeRange(0, attributedString.string.length)];
weakSelf.titleLabel.attributedText = attributedString;
weakSelf.maskTitleLabel.attributedText = attributedString;
};
}
- (JXCategoryCellSelectedAnimationBlock)preferredTitleColorAnimationBlock:(JXCategoryTitleCellModel *)cellModel {
__weak typeof(self) weakSelf = self;
return ^(CGFloat percent) {
if (cellModel.isSelected) {
//textColortitleNormalColortitleSelectedColor
cellModel.titleCurrentColor = [JXCategoryFactory interpolationColorFrom:cellModel.titleNormalColor to:cellModel.titleSelectedColor percent:percent];
} else {
//textColortitleSelectedColortitleNormalColor
cellModel.titleCurrentColor = [JXCategoryFactory interpolationColorFrom:cellModel.titleSelectedColor to:cellModel.titleNormalColor percent:percent];
}
weakSelf.titleLabel.textColor = cellModel.titleCurrentColor;
};
}
@end

View File

@@ -0,0 +1,44 @@
//
// JXCategoryTitleCellModel.h
// UI系列测试
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorCellModel.h"
#import <UIKit/UIKit.h>
#import "JXCategoryViewDefines.h"
@interface JXCategoryTitleCellModel : JXCategoryIndicatorCellModel
@property (nonatomic, copy) NSString *title;
@property (nonatomic, assign, readonly) CGFloat titleHeight;
@property (nonatomic, assign) NSInteger titleNumberOfLines;
@property (nonatomic, assign) CGFloat titleLabelVerticalOffset;
@property (nonatomic, strong) UIColor *titleNormalColor;
@property (nonatomic, strong) UIColor *titleCurrentColor;
@property (nonatomic, strong) UIColor *titleSelectedColor;
@property (nonatomic, strong) UIFont *titleFont;
@property (nonatomic, strong) UIFont *titleSelectedFont;
@property (nonatomic, assign, getter=isTitleLabelMaskEnabled) BOOL titleLabelMaskEnabled;
@property (nonatomic, assign, getter=isTitleLabelZoomEnabled) BOOL titleLabelZoomEnabled;
@property (nonatomic, assign) CGFloat titleLabelNormalZoomScale;
@property (nonatomic, assign) CGFloat titleLabelCurrentZoomScale;
@property (nonatomic, assign) CGFloat titleLabelSelectedZoomScale;
@property (nonatomic, assign) CGFloat titleLabelZoomSelectedVerticalOffset;
@property (nonatomic, assign, getter=isTitleLabelStrokeWidthEnabled) BOOL titleLabelStrokeWidthEnabled;
@property (nonatomic, assign) CGFloat titleLabelNormalStrokeWidth;
@property (nonatomic, assign) CGFloat titleLabelCurrentStrokeWidth;
@property (nonatomic, assign) CGFloat titleLabelSelectedStrokeWidth;
@property (nonatomic, assign) JXCategoryTitleLabelAnchorPointStyle titleLabelAnchorPointStyle;
@end

View File

@@ -0,0 +1,31 @@
//
// JXCategoryTitleCellModel.m
// UI
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryTitleCellModel.h"
@implementation JXCategoryTitleCellModel
- (void)setTitle:(NSString *)title {
_title = title;
[self updateNumberSizeWidthIfNeeded];
}
- (void)setTitleFont:(UIFont *)titleFont {
_titleFont = titleFont;
[self updateNumberSizeWidthIfNeeded];
}
- (void)updateNumberSizeWidthIfNeeded {
if (self.titleFont) {
_titleHeight = [self.title boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : self.titleFont} context:nil].size.height;
}
}
@end

View File

@@ -0,0 +1,63 @@
//
// JXCategoryView.h
// UI系列测试
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryIndicatorView.h"
#import "JXCategoryTitleCell.h"
#import "JXCategoryTitleCellModel.h"
#import "JXCategoryViewDefines.h"
@class JXCategoryTitleView;
@protocol JXCategoryTitleViewDataSource <NSObject>
@optional
// 如果将JXCategoryTitleView嵌套进UITableView的cell每次重用的时候JXCategoryTitleView进行reloadData时会重新计算所有的title宽度。所以该应用场景需要UITableView的cellModel缓存titles的文字宽度再通过该代理方法返回给JXCategoryTitleView。
// 如果实现了该方法就以该方法返回的宽度为准,不触发内部默认的文字宽度计算。
- (CGFloat)categoryTitleView:(JXCategoryTitleView *)titleView widthForTitle:(NSString *)title;
@end
@interface JXCategoryTitleView : JXCategoryIndicatorView
@property (nonatomic, weak) id<JXCategoryTitleViewDataSource> titleDataSource;
@property (nonatomic, strong) NSArray <NSString *>*titles;
@property (nonatomic, assign) NSInteger titleNumberOfLines; //默认1
@property (nonatomic, strong) UIColor *titleColor; //默认:[UIColor blackColor]
@property (nonatomic, strong) UIColor *titleSelectedColor; //默认:[UIColor redColor]
@property (nonatomic, strong) UIFont *titleFont; //默认:[UIFont systemFontOfSize:15]
@property (nonatomic, strong) UIFont *titleSelectedFont; //文字被选中的字体。默认与titleFont一样
@property (nonatomic, assign, getter=isTitleColorGradientEnabled) BOOL titleColorGradientEnabled; //默认NOtitle的颜色是否渐变过渡
@property (nonatomic, assign, getter=isTitleLabelMaskEnabled) BOOL titleLabelMaskEnabled; //默认NOtitleLabel是否遮罩过滤。
//----------------------titleLabelZoomEnabled-----------------------//
@property (nonatomic, assign, getter=isTitleLabelZoomEnabled) BOOL titleLabelZoomEnabled; //默认为NO。为YES时titleSelectedFont失效以titleFont为准。
@property (nonatomic, assign, getter=isTitleLabelZoomScrollGradientEnabled) BOOL titleLabelZoomScrollGradientEnabled; //手势滚动中是否需要更新状态。默认为YES
@property (nonatomic, assign) CGFloat titleLabelZoomScale; //默认1.2titleLabelZoomEnabled为YES才生效。是对字号的缩放比如titleFont的pointSize为10放大之后字号就是10*1.2=12。
@property (nonatomic, assign) CGFloat titleLabelZoomSelectedVerticalOffset; //titleLabelZoomEnabled设置为YES会对titleLabel进行transform缩放当titleLabelZoomScale过大时比如设置为2选中的文本被放大之后底部会有很大的空白从视觉上看就跟其他未选中的文本不在一个水平线上。这个时候就可以用这个值进行调整。
//----------------------titleLabelStrokeWidth-----------------------//
@property (nonatomic, assign, getter=isTitleLabelStrokeWidthEnabled) BOOL titleLabelStrokeWidthEnabled; //默认NO
@property (nonatomic, assign) CGFloat titleLabelSelectedStrokeWidth; //默认:-3用于控制字体的粗细底层通过NSStrokeWidthAttributeName实现。使用该属性务必让titleFont和titleSelectedFont设置为一样的
//----------------------titleLabel缩放中心位置-----------------------//
@property (nonatomic, assign) CGFloat titleLabelVerticalOffset; //titleLabel锚点垂直方向的位置偏移数值越大越偏离中心默认为0
@property (nonatomic, assign) JXCategoryTitleLabelAnchorPointStyle titleLabelAnchorPointStyle; //titleLabel锚点位置用于调整titleLabel缩放时的基准位置。默认为JXCategoryTitleLabelAnchorPointStyleCenter
@end

View File

@@ -0,0 +1,164 @@
//
// JXCategoryView.m
// UI
//
// Created by jiaxin on 2018/3/15.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryTitleView.h"
#import "JXCategoryFactory.h"
@implementation JXCategoryTitleView
- (void)initializeData {
[super initializeData];
_titleNumberOfLines = 1;
_titleLabelZoomEnabled = NO;
_titleLabelZoomScale = 1.2;
_titleColor = [UIColor blackColor];
_titleSelectedColor = [UIColor redColor];
_titleFont = [UIFont systemFontOfSize:15];
_titleColorGradientEnabled = NO;
_titleLabelMaskEnabled = NO;
_titleLabelZoomScrollGradientEnabled = YES;
_titleLabelStrokeWidthEnabled = NO;
_titleLabelSelectedStrokeWidth = -3;
_titleLabelVerticalOffset = 0;
_titleLabelAnchorPointStyle = JXCategoryTitleLabelAnchorPointStyleCenter;
}
- (UIFont *)titleSelectedFont {
if (_titleSelectedFont) {
return _titleSelectedFont;
}
return self.titleFont;
}
#pragma mark - Override
- (Class)preferredCellClass {
return [JXCategoryTitleCell class];
}
- (void)refreshDataSource {
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:self.titles.count];
for (int i = 0; i < self.titles.count; i++) {
JXCategoryTitleCellModel *cellModel = [[JXCategoryTitleCellModel alloc] init];
[tempArray addObject:cellModel];
}
self.dataSource = [NSArray arrayWithArray:tempArray];
}
- (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel {
[super refreshSelectedCellModel:selectedCellModel unselectedCellModel:unselectedCellModel];
JXCategoryTitleCellModel *myUnselectedCellModel = (JXCategoryTitleCellModel *)unselectedCellModel;
JXCategoryTitleCellModel *myselectedCellModel = (JXCategoryTitleCellModel *)selectedCellModel;
if (self.isSelectedAnimationEnabled && (selectedCellModel.selectedType == JXCategoryCellSelectedTypeClick || selectedCellModel.selectedType == JXCategoryCellSelectedTypeCode)) {
//cellcurrentcell
//1unselectedCell
//2selectedCellselectItemAtIndex
BOOL isUnselectedCellVisible = NO;
BOOL isSelectedCellVisible = NO;
NSArray *indexPaths = [self.collectionView indexPathsForVisibleItems];
for (NSIndexPath *indexPath in indexPaths) {
if (indexPath.item == myUnselectedCellModel.index) {
isUnselectedCellVisible = YES;
continue;
} else if (indexPath.item == myselectedCellModel.index) {
isSelectedCellVisible = YES;
continue;
}
}
if (!isUnselectedCellVisible) {
//unselectedCellcell
myUnselectedCellModel.titleCurrentColor = myUnselectedCellModel.titleNormalColor;
myUnselectedCellModel.titleLabelCurrentZoomScale = myUnselectedCellModel.titleLabelNormalZoomScale;
myUnselectedCellModel.titleLabelCurrentStrokeWidth = myUnselectedCellModel.titleLabelNormalStrokeWidth;
}
if (!isSelectedCellVisible) {
//selectedCellcell
myselectedCellModel.titleCurrentColor = myselectedCellModel.titleSelectedColor;
myselectedCellModel.titleLabelCurrentZoomScale = myselectedCellModel.titleLabelSelectedZoomScale;
myselectedCellModel.titleLabelCurrentStrokeWidth = myselectedCellModel.titleLabelSelectedStrokeWidth;
}
} else {
//
myselectedCellModel.titleCurrentColor = myselectedCellModel.titleSelectedColor;
myselectedCellModel.titleLabelCurrentZoomScale = myselectedCellModel.titleLabelSelectedZoomScale;
myselectedCellModel.titleLabelCurrentStrokeWidth = myselectedCellModel.titleLabelSelectedStrokeWidth;
myUnselectedCellModel.titleCurrentColor = myUnselectedCellModel.titleNormalColor;
myUnselectedCellModel.titleLabelCurrentZoomScale = myUnselectedCellModel.titleLabelNormalZoomScale;
myUnselectedCellModel.titleLabelCurrentStrokeWidth = myUnselectedCellModel.titleLabelNormalStrokeWidth;
}
}
- (void)refreshLeftCellModel:(JXCategoryBaseCellModel *)leftCellModel rightCellModel:(JXCategoryBaseCellModel *)rightCellModel ratio:(CGFloat)ratio {
[super refreshLeftCellModel:leftCellModel rightCellModel:rightCellModel ratio:ratio];
JXCategoryTitleCellModel *leftModel = (JXCategoryTitleCellModel *)leftCellModel;
JXCategoryTitleCellModel *rightModel = (JXCategoryTitleCellModel *)rightCellModel;
if (self.isTitleLabelZoomEnabled && self.isTitleLabelZoomScrollGradientEnabled) {
leftModel.titleLabelCurrentZoomScale = [JXCategoryFactory interpolationFrom:self.titleLabelZoomScale to:1.0 percent:ratio];
rightModel.titleLabelCurrentZoomScale = [JXCategoryFactory interpolationFrom:1.0 to:self.titleLabelZoomScale percent:ratio];
}
if (self.isTitleLabelStrokeWidthEnabled) {
leftModel.titleLabelCurrentStrokeWidth = [JXCategoryFactory interpolationFrom:leftModel.titleLabelSelectedStrokeWidth to:leftModel.titleLabelNormalStrokeWidth percent:ratio];
rightModel.titleLabelCurrentStrokeWidth = [JXCategoryFactory interpolationFrom:rightModel.titleLabelNormalStrokeWidth to:rightModel.titleLabelSelectedStrokeWidth percent:ratio];
}
if (self.isTitleColorGradientEnabled) {
leftModel.titleCurrentColor = [JXCategoryFactory interpolationColorFrom:self.titleSelectedColor to:self.titleColor percent:ratio];
rightModel.titleCurrentColor = [JXCategoryFactory interpolationColorFrom:self.titleColor to:self.titleSelectedColor percent:ratio];
}
}
- (CGFloat)preferredCellWidthAtIndex:(NSInteger)index {
if (self.cellWidth == JXCategoryViewAutomaticDimension) {
if (self.titleDataSource && [self.titleDataSource respondsToSelector:@selector(categoryTitleView:widthForTitle:)]) {
return [self.titleDataSource categoryTitleView:self widthForTitle:self.titles[index]];
} else {
return ceilf([self.titles[index] boundingRectWithSize:CGSizeMake(MAXFLOAT, self.bounds.size.height) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : self.titleFont} context:nil].size.width);
}
} else {
return self.cellWidth;
}
}
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index {
[super refreshCellModel:cellModel index:index];
JXCategoryTitleCellModel *model = (JXCategoryTitleCellModel *)cellModel;
model.title = self.titles[index];
model.titleNumberOfLines = self.titleNumberOfLines;
model.titleFont = self.titleFont;
model.titleSelectedFont = self.titleSelectedFont;
model.titleNormalColor = self.titleColor;
model.titleSelectedColor = self.titleSelectedColor;
model.titleLabelMaskEnabled = self.isTitleLabelMaskEnabled;
model.titleLabelZoomEnabled = self.isTitleLabelZoomEnabled;
model.titleLabelNormalZoomScale = 1;
model.titleLabelZoomSelectedVerticalOffset = self.titleLabelZoomSelectedVerticalOffset;
model.titleLabelSelectedZoomScale = self.titleLabelZoomScale;
model.titleLabelStrokeWidthEnabled = self.isTitleLabelStrokeWidthEnabled;
model.titleLabelNormalStrokeWidth = 0;
model.titleLabelSelectedStrokeWidth = self.titleLabelSelectedStrokeWidth;
model.titleLabelVerticalOffset = self.titleLabelVerticalOffset;
model.titleLabelAnchorPointStyle = self.titleLabelAnchorPointStyle;
if (index == self.selectedIndex) {
model.titleCurrentColor = model.titleSelectedColor;
model.titleLabelCurrentZoomScale = model.titleLabelSelectedZoomScale;
model.titleLabelCurrentStrokeWidth= model.titleLabelSelectedStrokeWidth;
}else {
model.titleCurrentColor = model.titleNormalColor;
model.titleLabelCurrentZoomScale = model.titleLabelNormalZoomScale;
model.titleLabelCurrentStrokeWidth = model.titleLabelNormalStrokeWidth;
}
}
@end

View File

@@ -0,0 +1,15 @@
//
// JXCategoryImageCell.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/8.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryTitleCell.h"
@interface JXCategoryTitleImageCell : JXCategoryTitleCell
@property (nonatomic, strong) UIImageView *imageView;
@end

View File

@@ -0,0 +1,148 @@
//
// JXCategoryImageCell.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/8.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryTitleImageCell.h"
#import "JXCategoryTitleImageCellModel.h"
@interface JXCategoryTitleImageCell()
@property (nonatomic, strong) id currentImageInfo;
@property (nonatomic, strong) NSString *currentImageName;
@property (nonatomic, strong) NSURL *currentImageURL;
@property (nonatomic, strong) UIStackView *stackView;
@property (nonatomic, strong) NSLayoutConstraint *imageViewWidthConstraint;
@property (nonatomic, strong) NSLayoutConstraint *imageViewHeightConstraint;
@end
@implementation JXCategoryTitleImageCell
- (void)prepareForReuse {
[super prepareForReuse];
self.currentImageInfo = nil;
self.currentImageName = nil;
self.currentImageURL = nil;
}
- (void)initializeViews {
[super initializeViews];
[self.titleLabel removeFromSuperview];
_imageView = [[UIImageView alloc] init];
self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
self.imageViewWidthConstraint = [self.imageView.widthAnchor constraintEqualToConstant:0];
self.imageViewWidthConstraint.active = YES;
self.imageViewHeightConstraint = [self.imageView.heightAnchor constraintEqualToConstant:0];
self.imageViewHeightConstraint.active = YES;
_stackView = [[UIStackView alloc] init];
self.stackView.alignment = UIStackViewAlignmentCenter;
[self.contentView addSubview:self.stackView];
self.stackView.translatesAutoresizingMaskIntoConstraints = NO;
[self.stackView.centerXAnchor constraintEqualToAnchor:self.contentView.centerXAnchor].active = YES;
[self.stackView.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor].active = YES;
}
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel {
[super reloadData:cellModel];
JXCategoryTitleImageCellModel *myCellModel = (JXCategoryTitleImageCellModel *)cellModel;
self.titleLabel.hidden = NO;
self.imageView.hidden = NO;
[self.stackView removeArrangedSubview:self.titleLabel];
[self.stackView removeArrangedSubview:self.imageView];
CGSize imageSize = myCellModel.imageSize;
self.imageViewWidthConstraint.constant = imageSize.width;
self.imageViewHeightConstraint.constant = imageSize.height;
self.stackView.spacing = myCellModel.titleImageSpacing;
switch (myCellModel.imageType) {
case JXCategoryTitleImageType_TopImage: {
self.stackView.axis = UILayoutConstraintAxisVertical;
[self.stackView addArrangedSubview:self.imageView];
[self.stackView addArrangedSubview:self.titleLabel];
break;
}
case JXCategoryTitleImageType_LeftImage: {
self.stackView.axis = UILayoutConstraintAxisHorizontal;
[self.stackView addArrangedSubview:self.imageView];
[self.stackView addArrangedSubview:self.titleLabel];
break;
}
case JXCategoryTitleImageType_BottomImage: {
self.stackView.axis = UILayoutConstraintAxisVertical;
[self.stackView addArrangedSubview:self.titleLabel];
[self.stackView addArrangedSubview:self.imageView];
break;
}
case JXCategoryTitleImageType_RightImage: {
self.stackView.axis = UILayoutConstraintAxisHorizontal;
[self.stackView addArrangedSubview:self.titleLabel];
[self.stackView addArrangedSubview:self.imageView];
break;
}
case JXCategoryTitleImageType_OnlyImage: {
self.titleLabel.hidden = YES;
[self.stackView addArrangedSubview:self.imageView];
break;
}
case JXCategoryTitleImageType_OnlyTitle: {
self.imageView.hidden = YES;
[self.stackView addArrangedSubview:self.titleLabel];
break;
}
}
//`- (void)reloadData:(JXCategoryBaseCellModel *)cellModel`
if (myCellModel.loadImageBlock != nil) {
id currentImageInfo = myCellModel.imageInfo;
if (myCellModel.isSelected) {
currentImageInfo = myCellModel.selectedImageInfo;
}
if (currentImageInfo && ![currentImageInfo isEqual:self.currentImageInfo]) {
self.currentImageInfo = currentImageInfo;
if (myCellModel.loadImageBlock) {
myCellModel.loadImageBlock(self.imageView, currentImageInfo);
}
}
}else {
NSString *currentImageName;
NSURL *currentImageURL;
if (myCellModel.imageName) {
currentImageName = myCellModel.imageName;
} else if (myCellModel.imageURL) {
currentImageURL = myCellModel.imageURL;
}
if (myCellModel.isSelected) {
if (myCellModel.selectedImageName) {
currentImageName = myCellModel.selectedImageName;
} else if (myCellModel.selectedImageURL) {
currentImageURL = myCellModel.selectedImageURL;
}
}
if (currentImageName && ![currentImageName isEqualToString:self.currentImageName]) {
self.currentImageName = currentImageName;
self.imageView.image = [UIImage imageNamed:currentImageName];
} else if (currentImageURL && ![currentImageURL.absoluteString isEqualToString:self.currentImageURL.absoluteString]) {
self.currentImageURL = currentImageURL;
if (myCellModel.loadImageCallback) {
myCellModel.loadImageCallback(self.imageView, currentImageURL);
}
}
}
if (myCellModel.isImageZoomEnabled) {
self.imageView.transform = CGAffineTransformMakeScale(myCellModel.imageZoomScale, myCellModel.imageZoomScale);
} else {
self.imageView.transform = CGAffineTransformIdentity;
}
}
@end

View File

@@ -0,0 +1,43 @@
//
// JXCategoryTitleImageCellModel.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/8.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryTitleCellModel.h"
typedef NS_ENUM(NSUInteger, JXCategoryTitleImageType) {
JXCategoryTitleImageType_TopImage = 0,
JXCategoryTitleImageType_LeftImage,
JXCategoryTitleImageType_BottomImage,
JXCategoryTitleImageType_RightImage,
JXCategoryTitleImageType_OnlyImage,
JXCategoryTitleImageType_OnlyTitle,
};
@interface JXCategoryTitleImageCellModel : JXCategoryTitleCellModel
@property (nonatomic, assign) JXCategoryTitleImageType imageType;
@property (nonatomic, strong) id imageInfo;
@property (nonatomic, strong) id selectedImageInfo;
@property (nonatomic, copy) void(^loadImageBlock)(UIImageView *imageView, id info);
@property (nonatomic, assign) CGSize imageSize; //默认CGSizeMake(20, 20)
@property (nonatomic, assign) CGFloat titleImageSpacing; //titleLabel和ImageView的间距默认5
@property (nonatomic, assign, getter=isImageZoomEnabled) BOOL imageZoomEnabled;
@property (nonatomic, assign) CGFloat imageZoomScale;
/// 以下属性将会被弃用
@property (nonatomic, copy) NSString *imageName; //加载bundle内的图片
@property (nonatomic, strong) NSURL *imageURL; //图片URL
@property (nonatomic, copy) NSString *selectedImageName;
@property (nonatomic, strong) NSURL *selectedImageURL;
@property (nonatomic, copy) void(^loadImageCallback)(UIImageView *imageView, NSURL *imageURL);
@end

View File

@@ -0,0 +1,13 @@
//
// JXCategoryTitleImageCellModel.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/8.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryTitleImageCellModel.h"
@implementation JXCategoryTitleImageCellModel
@end

View File

@@ -0,0 +1,42 @@
//
// JXCategoryTitleImageView.h
// JXCategoryView
//
// Created by jiaxin on 2018/8/8.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import "JXCategoryTitleView.h"
#import "JXCategoryTitleImageCell.h"
#import "JXCategoryTitleImageCellModel.h"
@interface JXCategoryTitleImageView : JXCategoryTitleView
//imageInfo数组可以传入imageName字符串或者image的URL地址等然后会通过loadImageBlock透传回来把imageView对于图片的加载过程完全交给使用者决定。
@property (nonatomic, strong) NSArray <id>*imageInfoArray;
@property (nonatomic, strong) NSArray <id>*selectedImageInfoArray;
@property (nonatomic, copy) void(^loadImageBlock)(UIImageView *imageView, id info);
//图片尺寸。默认CGSizeMake(20, 20)
@property (nonatomic, assign) CGSize imageSize;
//titleLabel和ImageView的间距默认5
@property (nonatomic, assign) CGFloat titleImageSpacing;
//图片是否缩放。默认为NO
@property (nonatomic, assign, getter=isImageZoomEnabled) BOOL imageZoomEnabled;
//图片缩放的最大scale。默认1.2imageZoomEnabled为YES才生效
@property (nonatomic, assign) CGFloat imageZoomScale;
//默认@[JXCategoryTitleImageType_LeftImage...]
@property (nonatomic, strong) NSArray <NSNumber *> *imageTypes;
//下面的属性将会被弃用,请使用`imageInfoArray`、`selectedImageInfoArray`、`loadImageBlock`属性完成需求。
//普通状态下的imageNames通过[UIImage imageNamed:]方法加载
@property (nonatomic, strong) NSArray <NSString *>*imageNames;
//选中状态下的imageNames通过[UIImage imageNamed:]方法加载。可选
@property (nonatomic, strong) NSArray <NSString *>*selectedImageNames;
//普通状态下的imageURLs通过loadImageCallback回调加载
@property (nonatomic, strong) NSArray <NSURL *>*imageURLs;
//选中状态下的selectedImageURLs通过loadImageCallback回调加载
@property (nonatomic, strong) NSArray <NSURL *>*selectedImageURLs;
//使用imageURL从远端下载图片进行加载建议使用SDWebImage等第三方库进行下载。
@property (nonatomic, copy) void(^loadImageCallback)(UIImageView *imageView, NSURL *imageURL);
@end

View File

@@ -0,0 +1,126 @@
//
// JXCategoryTitleImageView.m
// JXCategoryView
//
// Created by jiaxin on 2018/8/8.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXCategoryTitleImageView.h"
#import "JXCategoryTitleImageCell.h"
#import "JXCategoryTitleImageCellModel.h"
#import "JXCategoryFactory.h"
@implementation JXCategoryTitleImageView
- (void)dealloc {
self.loadImageBlock = nil;
self.loadImageCallback = nil;
}
- (void)initializeData {
[super initializeData];
_imageSize = CGSizeMake(20, 20);
_titleImageSpacing = 5;
_imageZoomEnabled = NO;
_imageZoomScale = 1.2;
}
- (Class)preferredCellClass {
return [JXCategoryTitleImageCell class];
}
- (void)refreshDataSource {
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:self.titles.count];
for (int i = 0; i < self.titles.count; i++) {
JXCategoryTitleImageCellModel *cellModel = [[JXCategoryTitleImageCellModel alloc] init];
[tempArray addObject:cellModel];
}
self.dataSource = [NSArray arrayWithArray:tempArray];
if (!self.imageTypes || (self.imageTypes.count == 0)) {
NSMutableArray *types = [NSMutableArray arrayWithCapacity:self.titles.count];
for (int i = 0; i< self.titles.count; i++) {
[types addObject:@(JXCategoryTitleImageType_LeftImage)];
}
self.imageTypes = [NSArray arrayWithArray:types];
}
}
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index {
[super refreshCellModel:cellModel index:index];
JXCategoryTitleImageCellModel *myCellModel = (JXCategoryTitleImageCellModel *)cellModel;
myCellModel.loadImageBlock = self.loadImageBlock;
myCellModel.loadImageCallback = self.loadImageCallback;
myCellModel.imageType = [self.imageTypes[index] integerValue];
myCellModel.imageSize = self.imageSize;
myCellModel.titleImageSpacing = self.titleImageSpacing;
if (self.imageInfoArray && self.imageInfoArray.count != 0) {
myCellModel.imageInfo = self.imageInfoArray[index];
}else if (self.imageNames && self.imageNames.count != 0) {
myCellModel.imageName = self.imageNames[index];
}else if (self.imageURLs && self.imageURLs.count != 0) {
myCellModel.imageURL = self.imageURLs[index];
}
if (self.selectedImageInfoArray && self.selectedImageInfoArray.count != 0) {
myCellModel.selectedImageInfo = self.selectedImageInfoArray[index];
}else if (self.selectedImageNames && self.selectedImageNames.count != 0) {
myCellModel.selectedImageName = self.selectedImageNames[index];
}else if (self.selectedImageURLs && self.selectedImageURLs.count != 0) {
myCellModel.selectedImageURL = self.selectedImageURLs[index];
}
myCellModel.imageZoomEnabled = self.imageZoomEnabled;
myCellModel.imageZoomScale = ((index == self.selectedIndex) ? self.imageZoomScale : 1.0);
}
- (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel {
[super refreshSelectedCellModel:selectedCellModel unselectedCellModel:unselectedCellModel];
JXCategoryTitleImageCellModel *myUnselectedCellModel = (JXCategoryTitleImageCellModel *)unselectedCellModel;
myUnselectedCellModel.imageZoomScale = 1.0;
JXCategoryTitleImageCellModel *myselectedCellModel = (JXCategoryTitleImageCellModel *)selectedCellModel;
myselectedCellModel.imageZoomScale = self.imageZoomScale;
}
- (void)refreshLeftCellModel:(JXCategoryBaseCellModel *)leftCellModel rightCellModel:(JXCategoryBaseCellModel *)rightCellModel ratio:(CGFloat)ratio {
[super refreshLeftCellModel:leftCellModel rightCellModel:rightCellModel ratio:ratio];
JXCategoryTitleImageCellModel *leftModel = (JXCategoryTitleImageCellModel *)leftCellModel;
JXCategoryTitleImageCellModel *rightModel = (JXCategoryTitleImageCellModel *)rightCellModel;
if (self.isImageZoomEnabled) {
leftModel.imageZoomScale = [JXCategoryFactory interpolationFrom:self.imageZoomScale to:1.0 percent:ratio];
rightModel.imageZoomScale = [JXCategoryFactory interpolationFrom:1.0 to:self.imageZoomScale percent:ratio];
}
}
- (CGFloat)preferredCellWidthAtIndex:(NSInteger)index {
if (self.cellWidth == JXCategoryViewAutomaticDimension) {
CGFloat titleWidth = [super preferredCellWidthAtIndex:index];
JXCategoryTitleImageType type = [self.imageTypes[index] integerValue];
CGFloat cellWidth = 0;
switch (type) {
case JXCategoryTitleImageType_OnlyTitle:
cellWidth = titleWidth;
break;
case JXCategoryTitleImageType_OnlyImage:
cellWidth = self.imageSize.width;
break;
case JXCategoryTitleImageType_LeftImage:
case JXCategoryTitleImageType_RightImage:
cellWidth = titleWidth + self.titleImageSpacing + self.imageSize.width;
break;
case JXCategoryTitleImageType_TopImage:
case JXCategoryTitleImageType_BottomImage:
cellWidth = MAX(titleWidth, self.imageSize.width);
break;
}
return cellWidth;
}
return self.cellWidth;
}
@end

View File

@@ -0,0 +1,17 @@
//
// JXCategoryTitleVerticalZoomCell.h
// JXCategoryView
//
// Created by jiaxin on 2019/2/14.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import "JXCategoryTitleCell.h"
NS_ASSUME_NONNULL_BEGIN
@interface JXCategoryTitleVerticalZoomCell : JXCategoryTitleCell
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,45 @@
//
// JXCategoryTitleVerticalZoomCell.m
// JXCategoryView
//
// Created by jiaxin on 2019/2/14.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import "JXCategoryTitleVerticalZoomCell.h"
#import "JXCategoryTitleVerticalZoomCellModel.h"
@implementation JXCategoryTitleVerticalZoomCell
- (void)reloadData:(JXCategoryBaseCellModel *)cellModel {
[super reloadData:cellModel];
JXCategoryTitleVerticalZoomCellModel *myCellModel = (JXCategoryTitleVerticalZoomCellModel *)cellModel;
if (myCellModel.isTitleLabelZoomEnabled) {
//fonttitleLabelZoomScaletransform
UIFont *maxScaleFont = [UIFont fontWithDescriptor:myCellModel.titleFont.fontDescriptor size:myCellModel.titleFont.pointSize*myCellModel.maxVerticalFontScale];
CGFloat baseScale = myCellModel.titleFont.lineHeight/maxScaleFont.lineHeight;
if (myCellModel.isSelectedAnimationEnabled && [self checkCanStartSelectedAnimation:myCellModel]) {
JXCategoryCellSelectedAnimationBlock block = [self preferredTitleZoomAnimationBlock:myCellModel baseScale:baseScale];
[self addSelectedAnimationBlock:block];
} else {
self.titleLabel.font = maxScaleFont;
self.maskTitleLabel.font = maxScaleFont;
CGAffineTransform currentTransform = CGAffineTransformMakeScale(baseScale*myCellModel.titleLabelCurrentZoomScale, baseScale*myCellModel.titleLabelCurrentZoomScale);
self.titleLabel.transform = currentTransform;
self.maskTitleLabel.transform = currentTransform;
}
} else {
if (myCellModel.isSelected) {
self.titleLabel.font = myCellModel.titleSelectedFont;
self.maskTitleLabel.font = myCellModel.titleSelectedFont;
}else {
self.titleLabel.font = myCellModel.titleFont;
self.maskTitleLabel.font = myCellModel.titleFont;
}
}
[self.titleLabel sizeToFit];
}
@end

View File

@@ -0,0 +1,19 @@
//
// JXCategoryTitleVerticalZoomCellModel.h
// JXCategoryView
//
// Created by jiaxin on 2019/2/14.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import "JXCategoryTitleCellModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface JXCategoryTitleVerticalZoomCellModel : JXCategoryTitleCellModel
@property (nonatomic, assign) CGFloat maxVerticalFontScale;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,13 @@
//
// JXCategoryTitleVerticalZoomCellModel.m
// JXCategoryView
//
// Created by jiaxin on 2019/2/14.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import "JXCategoryTitleVerticalZoomCellModel.h"
@implementation JXCategoryTitleVerticalZoomCellModel
@end

View File

@@ -0,0 +1,34 @@
//
// JXCategoryTitleVerticalZoomView.h
// JXCategoryView
//
// Created by jiaxin on 2019/2/14.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import "JXCategoryTitleView.h"
NS_ASSUME_NONNULL_BEGIN
/**
垂直方向的缩放值范围minVerticalFontScale~maxVerticalFontScale
垂直方向cellSpacing范围minVerticalCellSpacing~maxVerticalCellSpacing用于达到缩小时cell更加紧凑
根据UI设计师给你的参数去多次尝试设置上面的值来达到同样的效果。多尝试几次就知道每个属性设置之后的效果。
*/
@interface JXCategoryTitleVerticalZoomView : JXCategoryTitleView
@property (nonatomic, assign) CGFloat maxVerticalFontScale; //垂直方向最大的缩放值
@property (nonatomic, assign) CGFloat minVerticalFontScale; //垂直方向最小的缩放值
@property (nonatomic, assign) CGFloat maxVerticalCellSpacing; //垂直方向最大的cellSpacing
@property (nonatomic, assign) CGFloat minVerticalCellSpacing; //垂直方向最小的cellSpacing
/**
当前列表滚动时根据当前垂直方向categoryView高度变化的百分比刷新布局
@param percent 当前垂直方向categoryView高度变化百分比
*/
- (void)listDidScrollWithVerticalHeightPercent:(CGFloat)percent;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,94 @@
//
// JXCategoryTitleVerticalZoomView.m
// JXCategoryView
//
// Created by jiaxin on 2019/2/14.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import "JXCategoryTitleVerticalZoomView.h"
#import "JXCategoryTitleVerticalZoomCellModel.h"
#import "JXCategoryTitleVerticalZoomCell.h"
#import "JXCategoryFactory.h"
@interface JXCategoryTitleVerticalZoomView ()
@property (nonatomic, assign) CGFloat currentVerticalScale; //
@end
@implementation JXCategoryTitleVerticalZoomView
- (void)initializeData {
[super initializeData];
_maxVerticalFontScale = 2;
_minVerticalFontScale = 1.3;
_currentVerticalScale = _maxVerticalFontScale;
self.cellWidthZoomEnabled = YES;
self.cellWidthZoomScale = _maxVerticalFontScale;
self.contentEdgeInsetLeft = 15;
self.titleLabelZoomScale = _currentVerticalScale;
self.titleLabelZoomEnabled = YES;
self.selectedAnimationEnabled = YES;
_maxVerticalCellSpacing = 20;
_minVerticalCellSpacing = 10;
self.cellSpacing = _maxVerticalCellSpacing;
}
- (void)listDidScrollWithVerticalHeightPercent:(CGFloat)percent {
CGFloat currentScale = [JXCategoryFactory interpolationFrom:self.minVerticalFontScale to:self.maxVerticalFontScale percent:percent];
BOOL shouldReloadData = NO;
if (self.currentVerticalScale != currentScale) {
//reloadData
shouldReloadData = YES;
}
self.currentVerticalScale = currentScale;
self.cellWidthZoomScale = currentScale;
self.cellSpacing = [JXCategoryFactory interpolationFrom:self.minVerticalCellSpacing to:self.maxVerticalCellSpacing percent:percent];
if (shouldReloadData) {
[self refreshDataSource];
[self refreshState];
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
}
}
- (void)setCurrentVerticalScale:(CGFloat)currentVerticalScale {
_currentVerticalScale = currentVerticalScale;
self.titleLabelZoomScale = currentVerticalScale;
}
- (void)setMaxVerticalCellSpacing:(CGFloat)maxVerticalCellSpacing {
_maxVerticalCellSpacing = maxVerticalCellSpacing;
self.cellSpacing = maxVerticalCellSpacing;
}
- (void)setMaxVerticalFontScale:(CGFloat)maxVerticalFontScale {
_maxVerticalFontScale = maxVerticalFontScale;
self.titleLabelZoomScale = maxVerticalFontScale;
self.cellWidthZoomScale = maxVerticalFontScale;
}
- (Class)preferredCellClass {
return [JXCategoryTitleVerticalZoomCell class];
}
- (void)refreshDataSource {
NSMutableArray *tempArray = [NSMutableArray array];
for (int i = 0; i < self.titles.count; i++) {
JXCategoryTitleVerticalZoomCellModel *cellModel = [[JXCategoryTitleVerticalZoomCellModel alloc] init];
[tempArray addObject:cellModel];
}
self.dataSource = tempArray;
}
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index {
[super refreshCellModel:cellModel index:index];
JXCategoryTitleVerticalZoomCellModel *model = (JXCategoryTitleVerticalZoomCellModel *)cellModel;
model.maxVerticalFontScale = self.maxVerticalFontScale;
}
@end