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

@@ -10,6 +10,8 @@ target 'keyBoard' do
pod 'Bugly', :configurations => ['Release']
pod 'DZNEmptyDataSet', '1.8.1'
pod 'FLAnimatedImage', '~> 1.0.17'
pod 'JXPagingView/Pager', '~> 2.1.3'
pod 'JXCategoryView', '~> 1.6.8'
pod 'HWPanModal', '~> 0.9.9'
pod 'LSTPopView', '~> 0.3.10'
pod 'LookinServer', :configurations => ['Debug']

View File

@@ -18,6 +18,8 @@ PODS:
- DZNEmptyDataSet (1.8.1)
- FLAnimatedImage (1.0.17)
- HWPanModal (0.9.9)
- JXCategoryView (1.6.8)
- JXPagingView/Pager (2.1.3)
- LookinServer (1.2.8):
- LookinServer/Core (= 1.2.8)
- LookinServer/Core (1.2.8)
@@ -41,6 +43,8 @@ DEPENDENCIES:
- DZNEmptyDataSet (= 1.8.1)
- FLAnimatedImage (~> 1.0.17)
- HWPanModal (~> 0.9.9)
- JXCategoryView
- JXPagingView/Pager (~> 2.1.3)
- LookinServer
- LSTPopView (~> 0.3.10)
- Masonry (= 1.1.0)
@@ -56,6 +60,8 @@ SPEC REPOS:
- DZNEmptyDataSet
- FLAnimatedImage
- HWPanModal
- JXCategoryView
- JXPagingView
- LookinServer
- LSTPopView
- LSTTimer
@@ -71,6 +77,8 @@ SPEC CHECKSUMS:
DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7
FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b
HWPanModal: b57a6717d3cdcd666bff44f9dd2a5be9f4d6f5d2
JXCategoryView: 262d503acea0b1278c79a1c25b7332ffaef4d518
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
LookinServer: 1b2b61c6402ae29fa22182d48f5cd067b4e99e80
LSTPopView: 9379f00f6ce7d1fc620b50ab00ed3ef97b2d4d52
LSTTimer: caf8f02ff366ca175cf4c1778d26c166183c1b6f
@@ -80,6 +88,6 @@ SPEC CHECKSUMS:
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
SDWebImage: f29024626962457f3470184232766516dee8dfea
PODFILE CHECKSUM: c50284c86c4e30cee59c186e48767667019dcb61
PODFILE CHECKSUM: ce689000a7b9b24f5d74cc50a93350665c3af035
COCOAPODS: 1.16.2

21
Pods/JXCategoryView/LICENSE generated Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 暴走的鑫鑫
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

296
Pods/JXCategoryView/README.md generated Normal file
View File

@@ -0,0 +1,296 @@
<div align=center><img src="Example/Example/Images/JXCategoryView.png" width="405" height="63" /></div>
[![platform](https://img.shields.io/badge/platform-iOS-blue.svg?style=plastic)](#)
[![languages](https://img.shields.io/badge/language-objective--c-blue.svg)](#)
[![cocoapods](https://img.shields.io/badge/cocoapods-supported-4BC51D.svg?style=plastic)](https://cocoapods.org/pods/JXCategoryView)
[![support](https://img.shields.io/badge/support-ios%208%2B-orange.svg)](#)
A powerful and easy to use category view (segmentedcontrol, segmentview, pagingview, pagerview, pagecontrol) (腾讯新闻、今日头条、QQ 音乐、网易云音乐、京东、爱奇艺、腾讯视频、淘宝、天猫、简书、微博等所有主流 APP 分类切换滚动视图)
与其他的同类三方库对比的优点:
- 使用协议封装指示器逻辑,可以随心所欲地自定义指示器效果;
- 提供更加全面丰富、高度自定义的效果;
- 使用子类化管理 cell 样式,逻辑更清晰,扩展更简单;
- 高度封装列表容器,使用便捷,完美支持列表的生命周期调用;
## Swift版本
如果你在找 Swift 版本,请点击查看 [JXSegmentedView](https://github.com/pujiaxin33/JXSegmentedView)。
## 效果预览
### 指示器效果预览
说明 | Gif |
----|------|
LineView | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/LineView.gif" width="343" height="80"> |
LineView延长 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/JDLineStyle.gif" width="343" height="80"> |
LineView延长+偏移 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/IQIYILineStyle.gif" width="343" height="80"> |
LineView🌈彩虹风格 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/RainbowLineView.gif" width="343" height="80"> |
DotLineView点线效果 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/IndicatorCustomizeGuide.gif" width="334" height="88"> |
BallView QQ黏性红点 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/QQBall.gif" width="343" height="84"> |
TriangleView 三角形底部 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TriangleBottom.gif" width="343" height="80"> |
TriangleView 三角形顶部 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TriangleTop.gif" width="343" height="80"> |
BackgroundView椭圆形 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/BackgroundEllipseLayer.gif" width="343" height="80"> |
BackgroundView椭圆形+阴影 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/BackgroundViewShadow.gif" width="343" height="80"> |
BackgroundView长方形 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/Rectangle.gif" width="343" height="80"> |
BackgroundView遮罩有背景 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleMask.gif" width="343" height="80"> |
BackgroundView遮罩无背景 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleMaskNoBackgroundView.gif" width="343" height="80"> |
BackgroundView渐变色 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/GradientBGIndicatorView.gif" width="350" height="80"> |
ImageView底部(小船) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/IndicatorImageView.gif" width="343" height="137"> |
ImageView背景(最佳男歌手) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/BackgroundImageView.gif" width="343" height="80"> |
ImageView滚动效果(足球) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/Football.gif" width="343" height="135"> |
混合使用 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/Mixed.gif" width="343" height="80"> |
以下均支持上下位置切换:
`JXCategoryIndicatorLineView``JXCategoryIndicatorImageView``JXCategoryIndicatorBallView``JXCategoryIndicatorTriangleView`
### Cell样式效果预览
说明 | Gif |
----|------|
颜色渐变 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleColorGradient.gif" width="343" height="80"> |
大小缩放 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleZoom.gif" width="350" height="80"> |
大小缩放+底部锚点 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleLabelAnchorBottom.gif" width="350" height="80"> |
大小缩放+顶部锚点 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleLabelAnchorTop.gif" width="350" height="80"> |
大小缩放+字体粗细 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleZoomStrokeWidth.gif" width="350" height="80"> |
大小缩放+点击动画 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleZoomSelectedAnimation.gif" width="350" height="80"> |
大小缩放+cell宽度缩放 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleZoomCellWidth.gif" width="350" height="80"> |
TitleImage_Top | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleImageTop.gif" width="343" height="80"> |
TitleImage_Left | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleImageLeft.gif" width="343" height="80"> |
TitleImage_Bottom | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleImageBottom.gif" width="343" height="80"> |
TitleImage_Right | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleImageRight.gif" width="343" height="80"> |
cell图文混用 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/CellMixed.gif" width="343" height="90"> |
Image | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/CellImage.gif" width="343" height="80"> |
数字 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/Number.gif" width="343" height="80"> |
红点 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/CellRedDot.gif" width="343" height="80"> |
多行文本 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/MultiLineText.gif" width="350" height="80"> |
多行富文本 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/AttributeView.gif" width="343" height="80"> |
Cell背景色渐变 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/CellBackgroundColorGradient.gif" width="343" height="80"> |
分割线 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/SeparatorLine.gif" width="343" height="80"> |
### 特殊效果预览
说明 | Gif |
----|------|
数据源过少<br/> averageCellSpacingEnabled默认为YES | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/averageCellSpacingEnabledYES.gif" width="343" height="80"> |
数据源过少<br/> averageCellSpacingEnabled为NO | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/averageCellSpacingEnabledNO.gif" width="343" height="80"> |
SegmentedControl<br/>参考[`SegmentedControlViewController`](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/SegmentedControl/SegmentedControlViewController.m)类 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/SegmentedControl.gif" width="343" height="80"> |
导航栏使用<br/>参考[`NaviSegmentedControlViewController`](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/SegmentedControl/NaviSegmentedControlViewController.m)类 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/SegmentedControlNavi.gif" width="343" height="80"> |
嵌套使用<br/>参考[`NestViewController`](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/Nest/NestViewController.m)类 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/Nest.gif" width="343" height="272"> |
个人主页(上下左右滚动、header悬浮)<br/>参考[`PagingViewController`](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/PagingView/Example/PagingViewController.m)类<br/> 更多样式请点击查看[JXPagingView库](https://github.com/pujiaxin33/JXPagingView) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/UserProfile.gif" width="343" height="562"> |
垂直列表滚动<br/>参考[`VerticalListViewController`](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/VerticalListView/VerticalListViewController.m)类<br/> 高仿腾讯视频<br/>支持UITableView参考[`VerticalListTableViewController`](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/VerticalListView/VerticalListTableViewController.m)<br/>背景色异常是录屏软件bug | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/VerticalList.gif" width="343" height="607"> |
| 垂直缩放(仿网易圈圈、脉脉首页)<br/>参考[`ScrollZoomViewController`](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/ScrollZoom/ScrollZoomViewController.m)类 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/VerticalZoomTitle.gif" width="350" height="306"> |
数据源刷新&列表数据加载<br/>参考[`LoadDataListContainerViewController`](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/LoadData/LoadDataListContainerViewController.m)类 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/LoadData.gif" width="343" height="619"> |
上下滚动隐藏导航栏 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/ScrollUp.gif" width="336" height="354"> |
京东首页-滚动渐变变小 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/jingdo_scroll_small.gif" width="305" height="599"> |
### 自定义效果预览
收录来自其他使用者的自定义示例,这些自定义类只在 Demo 项目里面Pod 库并没有这些文件。所以,如果你需要使用这些自定义效果,请通过文件导入的方式。
目的:
- 参考学习如何自定义;
- 直接修改自定义示例类以快速实现自己的需求。
欢迎提 PullRequest 进行收录你的自定义效果。
| 说明 | Gif |
| ----|------|
| Spring动画指示器 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/SpringIndicator.gif" width="336" height="70"> |
| 富文本数量cell | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/CountCell.gif" width="336" height="70"> |
| 左右对齐指示器 | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/AlignmentIndicator.gif" width="336" height="70"> |
| 秒杀时间线cell | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/timeline.gif" width="336" height="70"> |
| 京东商品排序cell | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/JDProductSort.gif" width="336" height="70"> |
| title背景块cell | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/TitleBackgroundExample.gif" width="336" height="70"> |
## 要求
- iOS 9.0+
- Xcode 9+
- Objective-C
## 安装
### 手动
Clone 代码,把 Sources 文件夹拖入项目,`#import "JXCategoryView.h"` 就可以使用了。
### CocoaPods
```ruby
target '<Your Target Name>' do
pod 'JXCategoryView'
end
```
先执行 `pod repo update`,再执行 `pod install`
## 结构图
<img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/JXCategoryViewStructure.png" width="933" height="482">
## 使用
### JXCategoryTitleView 使用示例
1. 初始化 `JXCategoryTitleView`
```Objective-C
self.categoryView = [[JXCategoryTitleView alloc] initWithFrame:CGRectMake(0, 0, WindowsSize.width, 50)];
self.categoryView.delegate = self;
[self.view addSubview:self.categoryView];
```
2. 配置 `JXCategoryTitleView` 的属性:
```Objective-C
self.categoryView.titles = @[@"螃蟹", @"麻辣小龙虾", @"苹果"...];
self.categoryView.titleColorGradientEnabled = YES;
```
3. 添加指示器:
```Objective-C
JXCategoryIndicatorLineView *lineView = [[JXCategoryIndicatorLineView alloc] init];
lineView.indicatorColor = [UIColor redColor];
lineView.indicatorWidth = JXCategoryViewAutomaticDimension;
self.categoryView.indicators = @[lineView];
```
4. 实现 `JXCategoryViewDelegate` 代理(可选)
```Objective-C
// 点击选中或者滚动选中都会调用该方法。适用于只关心选中事件,不关心具体是点击还是滚动选中的。
- (void)categoryView:(JXCategoryBaseView *)categoryView didSelectedItemAtIndex:(NSInteger)index;
// 点击选中的情况才会调用该方法
- (void)categoryView:(JXCategoryBaseView *)categoryView didClickSelectedItemAtIndex:(NSInteger)index;
// 滚动选中的情况才会调用该方法
- (void)categoryView:(JXCategoryBaseView *)categoryView didScrollSelectedItemAtIndex:(NSInteger)index;
// 正在滚动中的回调
- (void)categoryView:(JXCategoryBaseView *)categoryView scrollingFromLeftIndex:(NSInteger)leftIndex toRightIndex:(NSInteger)rightIndex ratio:(CGFloat)ratio;
```
### 列表容器使用示例
#### `JXCategoryListContainerView` 封装类使用示例
`JXCategoryListContainerView` 是对列表视图高度封装的类,具有以下优点:
- 相对于直接使用 `UIScrollView` 自定义,封装度高、代码集中、使用简单;
- 列表懒加载:当显示某个列表的时候,才进行列表初始化。而不是一次性加载全部列表,性能更优;
- 支持列表的 `willAppear`、`didAppear`、`willDisappear`、`didDisappear` 生命周期方法调用;
1. 初始化 `JXCategoryListContainerView` 并关联到 `categoryView`
```Objective-C
self.listContainerView = [[JXCategoryListContainerView alloc] initWithType:JXCategoryListContainerType_ScrollView delegate:self];
[self.view addSubview:self.listContainerView];
// 关联到 categoryView
self.categoryView.listContainer = self.listContainerView;
```
2. 实现 `JXCategoryListContainerViewDelegate` 代理方法:
```Objective-C
// 返回列表的数量
- (NSInteger)numberOfListsInlistContainerView:(JXCategoryListContainerView *)listContainerView {
return self.titles.count;
}
// 根据下标 index 返回对应遵守并实现 `JXCategoryListContentViewDelegate` 协议的列表实例
- (id<JXCategoryListContentViewDelegate>)listContainerView:(JXCategoryListContainerView *)listContainerView initListForIndex:(NSInteger)index {
return [[ListViewController alloc] init];
}
```
3. 列表实现 `JXCategoryListContentViewDelegate` 代理方法
不管列表是 `UIView` 还是 `UIViewController` 都可以,提高使用灵活性,更便于现有的业务接入。
```Objective-C
// 返回列表视图
// 如果列表是 VC就返回 VC.view
// 如果列表是 View就返回 View 自己
- (UIView *)listView {
return self.view;
}
```
具体点击 [LoadDataListContainerViewController](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/LoadData/LoadDataListContainerViewController.m) 查看源代码了解
#### 直接使用 UIScrollView 自定义
因为代码量较多且分散,所有不推荐使用该方法。要正确使用需要注意的地方比较多,尤其对于刚接触 iOS 的同学来说不太友好。
不直接贴代码了,具体点击 [LoadDataListCustomViewController](https://github.com/pujiaxin33/JXCategoryView/tree/master/Example/Example/Examples/LoadData/LoadDataListCustomViewController.m) 查看源代码了解。
## 常见问题和答案
❗️❗️❗️这里面包含了许多常见问题和答案,使用之前请务必浏览此文档,或者遇到问题先看此文档❗️❗️❗️
[常见问题和答案总文档](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md)
- [个人主页效果更丰富的示例:JXPagingView](https://github.com/pujiaxin33/JXPagingView)
- [侧滑手势处理](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BE%A7%E6%BB%91%E6%89%8B%E5%8A%BF%E5%A4%84%E7%90%86.md)
- [列表的生命周期方法处理](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E5%88%97%E8%A1%A8%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E6%96%B9%E6%B3%95%E5%A4%84%E7%90%86.md)
- [`JXCategoryListContainerType`的`scrollView`和`collectionView`对比](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#jxcategorylistcontainertype%E7%9A%84scrollview%E5%92%8Ccollectionview%E5%AF%B9%E6%AF%94)
- [cell左滑删除](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#cell%E5%B7%A6%E6%BB%91%E5%88%A0%E9%99%A4)
- [`FDFullscreenPopGesture`等全屏手势处理](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E5%85%A8%E5%B1%8F%E6%89%8B%E5%8A%BF%E5%A4%84%E7%90%86.md)
- [JXCategoryView数据源刷新](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#jxcategoryview%E6%95%B0%E6%8D%AE%E6%BA%90%E5%88%B7%E6%96%B0)
- [`reloadDataWithoutListContainer`方法使用说明](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#reloaddatawithoutlistcontainer%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)
- [listContainer或contentScrollView关联说明](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#listcontainer%E6%88%96contentscrollview%E5%85%B3%E8%81%94%E8%AF%B4%E6%98%8E)
- [点击切换列表的动画控制](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E7%82%B9%E5%87%BB%E5%88%87%E6%8D%A2%E5%88%97%E8%A1%A8%E7%9A%84%E5%8A%A8%E7%94%BB%E6%8E%A7%E5%88%B6)
- [列表cell点击跳转示例](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E5%88%97%E8%A1%A8cell%E7%82%B9%E5%87%BB%E8%B7%B3%E8%BD%AC%E7%A4%BA%E4%BE%8B)
- [列表调用`presentViewController`方法](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E5%88%97%E8%A1%A8%E8%B0%83%E7%94%A8presentviewcontroller%E6%96%B9%E6%B3%95)
- [代码选中指定index](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E4%BB%A3%E7%A0%81%E9%80%89%E4%B8%AD%E6%8C%87%E5%AE%9Aindex)
- [JXCategoryView.collectionView高度取整说明](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#jxcategoryviewcollectionview%E9%AB%98%E5%BA%A6%E5%8F%96%E6%95%B4%E8%AF%B4%E6%98%8E)
- [对父VC的automaticallyAdjustsScrollViewInsets属性设置为NO](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E5%AF%B9%E7%88%B6vc%E7%9A%84automaticallyadjustsscrollviewinsets%E5%B1%9E%E6%80%A7%E8%AE%BE%E7%BD%AE%E4%B8%BAno)
- [`JXCategoryListContainerView`内部使用`UIViewController`当做列表容器使用说明](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#jxcategorylistcontainerview%E5%86%85%E9%83%A8%E4%BD%BF%E7%94%A8uiviewcontroller%E5%BD%93%E5%81%9A%E5%88%97%E8%A1%A8%E5%AE%B9%E5%99%A8%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)
- [使用多行文本](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E4%BD%BF%E7%94%A8%E5%A4%9A%E8%A1%8C%E6%96%87%E6%9C%AC)
- [列表容器禁止左右滑动](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E5%88%97%E8%A1%A8%E5%AE%B9%E5%99%A8%E7%A6%81%E6%AD%A2%E5%B7%A6%E5%8F%B3%E6%BB%91%E5%8A%A8)
- [单个cell刷新 ](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E5%8D%95%E4%B8%AAcell%E5%88%B7%E6%96%B0)
- [点击item时指示器和列表滚动时效果一致](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E7%82%B9%E5%87%BBitem%E6%97%B6%E6%8C%87%E7%A4%BA%E5%99%A8%E5%92%8C%E5%88%97%E8%A1%A8%E6%BB%9A%E5%8A%A8%E6%97%B6%E6%95%88%E6%9E%9C%E4%B8%80%E8%87%B4)
- [自定义建议](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BB%BA%E8%AE%AE)
## 指示器样式自定义
- 需要继承 `JXCategoryIndicatorProtocol` 协议,点击参看 [JXCategoryIndicatorProtocol](https://github.com/pujiaxin33/JXCategoryView/blob/master/Sources/Common/JXCategoryIndicatorProtocol.h)
- 提供了继承 `JXCategoryIndicatorProtocol` 协议的基类 `JXCategoryIndicatorComponentView`,里面提供了许多基础属性。点击参看 [JXCategoryIndicatorComponentView](https://github.com/pujiaxin33/JXCategoryView/blob/master/Sources/Indicator/IndicatorViews/JXCategoryIndicatorComponentView.m)
- 自定义指示器,请参考已实现的指示器视图,多尝试、多思考,再有问题请提 Issue 或加入反馈 QQ 群。
## Cell 自定义
- 任何子类化需求view、cell、cellModel 三个都要子类化,即使某个子类 cell 什么事情都不做。用于维护继承链,以免以后子类化都不知道要继承谁了;
- 如果你想完全自定义 cell 里面的内容,那就继承 `JXCategoryIndicatorView`、`JXCategoryIndicatorCell、`JXCategoryIndicatorCellModel`,就像`JXCategoryTitleView``JXCategoryTitleCell、`JXCategoryTitleCellModel`那样去做;
- 如果你只是在父类进行一些微调,那就继承目标 view、cell、cellModel对 cell 原有控件微调、或者加入新的控件皆可。就像 `JXCategoryTitleImageView` 系列、`JXCategoryTitleAttributeView` 系列那样去做;
- Cell 自定义,请参考已实现的 cell 样式,多尝试、多思考,再有问题请提 Issue 或加入反馈 QQ 群
## 常用属性说明
[常用属性说明文档地址](https://github.com/pujiaxin33/JXCategoryView/blob/master/Document/%E5%B8%B8%E7%94%A8%E5%B1%9E%E6%80%A7%E8%AF%B4%E6%98%8E.md)
## 更新记录
- 2018.8.21 发布1.0.0版本更新内容使用POP面向协议编程重构指示器视图[迁移指南](https://github.com/pujiaxin33/JXCategoryView/blob/master/Migration/1.0.0.md)
- 2018.8.22 发布1.0.1版本更新内容删除zoomEnabled,新增titleLabelZoomEnabled、imageZoomEnabled;
- 2018.8.23 发布1.0.2版本更新内容添加cellWidthZoomEnabled实现腾讯视频效果;
- 2018.8.24 发布1.0.3版本更新内容添加垂直列表滚动效果、指示器添加verticalMargin属性、JXCategoryViewDelegate代理方法优化;
- 2018.9.4 发布1.0.4版本更新内容修复bug、添加cell图文混用示例;
- 2018.12.19 发布1.1.7版本,更新内容:添加`JXCategoryListContainerView`,高度封装列表逻辑,支持懒加载列表,提升初始化性能;
- 2019.1.24 发布1.2.2版本,更新内容:非兼容更新接口`- (BOOL)selectCellAtIndex:(NSInteger)index selectedType:(JXCategoryCellSelectedType)selectedType`,自定义有用到该接口的请及时更新。
- 2019.6.21 发布1.3.13版本,更新内容:将`JXCategoryListCollectionContainerView.dataSource`移动到m实现文件添加`- (instancetype)initWithDataSource:(id<JXCategoryListCollectionContainerViewDataSource>)dataSource`初始化方法。
- 2019.7.20 发布1.3.16版本,删除代理方法`- (void)categoryView:(JXCategoryBaseView *)categoryView didClickedItemContentScrollViewTransitionToIndex:(NSInteger)index;`,请使用`contentScrollViewClickTransitionAnimationEnabled`属性。`JXCategoryTitleVerticalZoomView`进行了重构,内容左边距只需要使用`contentEdgeLeft`属性即可。
- 2019.9.11 发布1.4.0版本,删除一波被标记为弃用的属性和方法;完善列表的生命周期方法的调用;`JXCategoryListCollectionContainerView`类新增和必须要调用`- (void)scrollingFromLeftIndex:(NSInteger)leftIndex toRightIndex:(NSInteger)rightIndex ratio:(CGFloat)ratio selectedIndex:(NSInteger)selectedIndex`和`- (void)didClickSelectedItemAtIndex:(NSInteger)index`两个方法。
- 2019.9.19 发布1.5.0版本,重构列表容器,具体修改请参考[1.5.0版本迁移指南](https://github.com/pujiaxin33/JXCategoryView/blob/master/Migration/1.5.0%E7%89%88%E6%9C%AC%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97.md)
## 补充
如果刚开始使用`JXCategoryView`当开发过程中需要支持某种特性时请务必先搜索使用文档或者源代码。确认是否已经实现支持了想要的特性。请别不要文档和源代码都没有看就直接提问这对于大家都是一种时间浪费。如果没有支持想要的特性欢迎提Issue讨论或者自己实现提一个PullRequest。
该仓库保持随时更新,对于主流新的分类选择效果会第一时间支持。使用过程中,有任何建议或问题,可以通过以下方式联系我:</br>
邮箱317437084@qq.com </br>
QQ群 112440473
<img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCategoryView/JXCategoryViewQQGroupTwo.JPG" width="300" height="411">
喜欢就star❤一下吧
## License
JXCategoryView is released under the MIT license.

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

21
Pods/JXPagingView/LICENSE generated Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 暴走的鑫鑫
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

294
Pods/JXPagingView/README.md generated Normal file
View File

@@ -0,0 +1,294 @@
# JXPagingView
类似微博主页、简书主页、QQ联系人页面等效果。多页面嵌套既可以上下滑动也可以左右滑动切换页面。支持HeaderView悬浮、支持下拉刷新、上拉加载更多。
## 功能特点
- 支持OC与Swift;
- 支持列表懒加载,等到列表真正显示的时候才加载,而不是一次性加载所有列表;
- 支持首页下拉刷新、列表视图下拉刷新、列表视图上拉加载更多;
- 支持悬浮SectionHeader的垂直位置调整
- 支持从顶部用力往上滚动,下面的列表会跟着滚动,而不会突然卡主,需要使用`JXPagerSmoothView`类;
- 列表封装简洁,只要遵从`JXPagingViewListViewDelegate`协议即可。UIView、UIViewController等都可以
- 使用JXCategoryView/JXSegmentedView分类控制器几乎支持所有主流效果、高度自定义、可灵活扩展
- 支持横竖屏切换;
- 支持点击状态栏滚动当前列表到顶部;
- 支持列表显示和消失的生命周期方法;
- isListHorizontalScrollEnabled属性控制列表是否可以左右滑动默认YES
- 支持`FDFullscreenPopGesture`等全屏手势兼容处理;
## 预览
| 效果 | 预览图 |
|-------|-------|
| **头图缩放** <br/>参考[ZoomViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/Zoom/ZoomViewController.m)类 | ![Zoom](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/Zoom.gif) |
| **主页下拉刷新&列表上拉加载更多** <br/>参考[RefreshViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/Refresh/RefreshViewController.m)类 | ![Refresh](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/Refresh.gif) |
| **列表下拉刷新** <br/>参考[ListRefreshViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/Refresh/ListRefreshViewController.m)类 | ![Refresh](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/ListRefresh.gif) |
| **悬浮sectionHeader位置调整** | ![Refresh](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/PinSectionHeaderPosition.gif) |
| **导航栏隐藏** <br/> 参考[NaviBarHiddenViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/NavigationBarHidden/NaviBarHiddenViewController.m)类 | ![Refresh](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/NaviHidden.gif) |
| **CollectionView列表示例**<br/>参考[CollectionViewViewController.swift](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagingViewExample/JXPagingViewExample/Example/CollectionView/CollectionViewViewController.swift)类 <br/> 只有swift的demo工程有该示例 | ![Refresh](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/CollectionViewList.gif) |
| **HeaderView更新高度示例**<br/> 参考[HeightChangeAnimationViewController.swift](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagingViewExample/JXPagingViewExample/Example/HeightChange/HeightChangeAnimationViewController.swift)类 <br/> 只有swift demo工程才有该示例 | ![Refresh](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/HeaderViewHeightChange.gif) |
| **PagingView嵌套CategoryView** <br/> 参考[NestViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/Nest/NestViewController.m)类 <br/> 只有 **OC!OC!OC!** 的demo工程才有该示例 <br/> 操作比较特殊,如果需要此效果,<br/> 请认真参考源码,有问题多试试 <br/> 参考NestViewController.h类 | ![Nest](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/Nest.gif) |
| **CategoryView嵌套PagingView** <br/> 参考[NestViewController.swift](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagingViewExample/JXPagingViewExample/Example/CategoryNestPaging/NestViewController.swift)类 <br/> 只有 **Swift!Swift!Swift!** 的demo工程才有该示例 <br/> 操作比较特殊,如果需要此效果,<br/> 请认真参考源码,有问题多试试 <br/> 参考NestViewController.swift类 | ![Nest](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/CategoryNestPaging.gif) |
| **点击状态栏** | ![Zoom](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/StatusBarClicked.gif) |
| **横竖屏旋转** | ![Zoom](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/ScreenRotate.gif) |
| **JXPageListView**<br/> 顶部需要自定义cell的场景类似于电商APP首页滑动到列表最底部才是分类控制器 <br/> 该效果是另一个库,点击查看[JXPageListView](https://github.com/pujiaxin33/JXPageListView) <br/> 该效果是另一个库,点击查看[JXPageListView](https://github.com/pujiaxin33/JXPageListView) <br/> 该效果是另一个库,点击查看[JXPageListView](https://github.com/pujiaxin33/JXPageListView) | ![list](https://github.com/pujiaxin33/JXPageListView/blob/master/JXPageListView/Gif/headerLoading.gif) |
| **JXPagerSmoothView**<br/> 类似淘宝、转转首页 <br/> 从顶部用力往上滚动,下面的列表会继续滚动 | ![smooth](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXPaingView/smooth.gif) |
## 安装
### 手动
**Swift版本** Clone代码拖入JXPagingView-Swift文件夹使用`JXPagingView`类;
**OC版本** Clone代码拖入JXPagerView文件夹使用`JXPagerView`类;
### CocoaPods
- **Swift版本**
支持swift版本5.0+
```ruby
target '<Your Target Name>' do
pod 'JXPagingView/Paging'
end
```
- **OC版本**
```ruby
target '<Your Target Name>' do
pod 'JXPagingView/Pager'
end
```
Swift与OC的仓库地址不一样请注意选择
`pod repo update`然后再`pod install`
## 使用
swift版本使用类似只是类名及相关API更改为`JXPagingView`具体细节请查看Swfit工程。
### 1、初始化`JXCategoryTitleView`和`JXPagerView`
```Objective-C
self.categoryView = [[JXCategoryTitleView alloc] initWithFrame:frame];
//配置categoryView细节参考源码
self.pagerView = [[JXPagerView alloc] initWithDelegate:self];
[self.view addSubview:self.pagerView];
//⚠将pagerView的listContainerView和categoryView.listContainer进行关联这样列表就可以和categoryView联动了。⚠
self.categoryView.listContainer = (id<JXCategoryViewListContainer>)self.pagerView.listContainerView;
```
**Swift版本列表关联代码**
```Swift
//给JXPagingListContainerView添加extension表示遵从JXSegmentedViewListContainer的协议
extension JXPagingListContainerView: JXSegmentedViewListContainer {}
//⚠将pagingView的listContainerView和segmentedView.listContainer进行关联这样列表就可以和categoryView联动了。⚠
segmentedView.listContainer = pagingView.listContainerView
```
### 2、实现`JXPagerViewDelegate`协议
```Objective-C
/**
返回tableHeaderView的高度因为内部需要比对判断只能是整型数
*/
- (NSUInteger)tableHeaderViewHeightInPagerView:(JXPagerView *)pagerView {
return JXTableHeaderViewHeight;
}
/**
返回tableHeaderView
*/
- (UIView *)tableHeaderViewInPagerView:(JXPagerView *)pagerView {
return self.userHeaderView;
}
/**
返回悬浮HeaderView的高度因为内部需要比对判断只能是整型数
*/
- (NSUInteger)heightForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
return JXheightForHeaderInSection;
}
/**
返回悬浮HeaderView
*/
- (UIView *)viewForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
return self.categoryView;
}
/**
返回列表的数量
*/
- (NSInteger)numberOfListsInPagerView:(JXPagerView *)pagerView {
//和categoryView的item数量一致
return self.titles.count;
}
/**
根据index初始化一个对应列表实例。注意一定要是新生成的实例
只要遵循JXPagerViewListViewDelegate即可无论你返回的是UIView还是UIViewController都可以。
*/
- (id<JXPagerViewListViewDelegate>)pagerView:(JXPagerView *)pagerView initListAtIndex:(NSInteger)index {
TestListBaseView *listView = [[TestListBaseView alloc] init];
if (index == 0) {
listView.dataSource = @[@"橡胶火箭", @"橡胶火箭炮", @"橡胶机关枪"...].mutableCopy;
}else if (index == 1) {
listView.dataSource = @[@"吃烤肉", @"吃鸡腿肉", @"吃牛肉", @"各种肉"].mutableCopy;
}else {
listView.dataSource = @[@"【剑士】罗罗诺亚·索隆", @"【航海士】娜美", @"【狙击手】乌索普"...].mutableCopy;
}
[listView beginFirstRefresh];
return listView;
}
```
### 3、实现`JXPagerViewListViewDelegate`协议
列表可以是任意类UIView、UIViewController等等都可以只要实现了`JXPagerViewListViewDelegate`协议就行。
⚠️⚠️⚠️一定要保证`scrollCallback`的正确回调,许多朋友都容易疏忽这一点,导致异常,务必重点注意!
下面的使用代码参考的是`TestListBaseView`类
```Objective-C
/**
返回listView。如果是vc包裹的就是vc.view如果是自定义view包裹的就是自定义view自己。
*/
- (UIView *)listView {
return self;
}
/**
返回listView内部持有的UIScrollView或UITableView或UICollectionView
主要用于mainTableView已经显示了headerlistView的contentOffset需要重置时内部需要访问到外部传入进来的listView内的scrollView
*/
- (UIScrollView *)listScrollView {
return self.tableView;
}
/**
当listView内部持有的UIScrollView或UITableView或UICollectionView的代理方法`scrollViewDidScroll`回调时需要调用该代理方法传入的callback
*/
- (void)listViewDidScrollCallback:(void (^)(UIScrollView *))callback {
self.scrollCallback = callback;
}
```
### 4、列表回调处理
`TestListBaseView`在其`tableView`的滚动回调中通过调用上面持有的scrollCallback把列表的滚动事件回调给JXPagerView内部。
```Objective-C
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
!self.scrollCallback ?: self.scrollCallback(scrollView);
}
```
## 实现原理
[实现原理](https://github.com/pujiaxin33/JXPagingView/blob/master/Document/JXPagingView%E5%8E%9F%E7%90%86.md)
## `JXPagerSmoothView`
如果你需要类似于**淘宝**、**转转**首页从顶部header用力往上滚动之后下面的列表会跟着滚动的效果。因为`JXPagerView`的实现原理限制当用户从顶部header的位置用力往上滚动`JXPagerView`会在`JXCategoryView`刚好在顶部的时候突然停住。这个时候就需要使用`JXPagerSmoothView`swift版本叫`JXPagingSmoothView`。
因为与`JXPagerView`的原理完全不同所以各自会有一些特性的区别但是从使用体验来说是完全一致的。具体使用细节请参考demo示例。
实现原理参考[JXPagerSmoothView文章解析](https://juejin.im/post/5ddb2fe4f265da7def5424c7)
## 特殊说明
### JXCategoryView、JXSegmentedView
悬浮的HeaderView用的是我写的[OC版本-JXCategoryView](https://github.com/pujiaxin33/JXCategoryView) 、[Swift版本-JXSegmentedView](https://github.com/pujiaxin33/JXSegmentedView)。几乎实现了所有主流效果,而且非常容易自定义扩展,强烈推荐阅读。
### 头图缩放说明
头图缩放原理,参考这个库:[JXTableViewZoomHeaderImageView](https://github.com/pujiaxin33/JXTableViewZoomHeaderImageView)
### 列表下拉刷新说明
需要使用`JXPagerListRefreshView`类(是`JXPagerView`的子类)
### JXPagerListContainerType说明
UIScrollView优势没有其他副作用。劣势实时的视图内存占用相对大一点因为所有加载之后的列表视图都在视图层级里面。
UICollectionView优势因为列表被添加到cell上实时的视图内存占用更少适合内存要求特别高的场景。劣势因为cell重用机制的问题导致列表被移除屏幕外之后会被放入缓存区而不存在于视图层级中。如果刚好你的列表使用了下拉刷新视图在快速切换过程中就会导致下拉刷新回调不成功的问题。使用MJRefresh会出现此问题一句话概括使用CollectionView的时候就不要让列表使用下拉刷新加载。
### 关于下方列表视图的代理方法`- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath`有时候需要点击两次才回调
出现步骤当手指放在下方列表视图往下拉直到TableHeaderView完全显示。
原因经过上面的步骤之后手指已经离开屏幕且列表视图已经完全静止UIScrollView的isDragging属性却依然是true。就导致了后续的第一次点击让系统认为当前UIScrollView依然在滚动该点击就让UIScrollView停止下来没有继续转发给UITableView就没有转化成didSelectRow事件。
解决方案经过N种尝试之后还是没有回避掉系统的`isDragging`异常为true的bug。大家可以在自定义cell最下方放置一个与cell同大小的button把button的touchUpInside事件当做`didSelectRow`的回调。因为UIButton在响应链中的优先级要高于UIGestureRecognizer。
代码:请参考`TestTableViewCell`类的配置。
### 指定默认选中index
默认显示index=2的列表代码如下
```
self.pagerView.defaultSelectedIndex = 2;
self.categoryView.defaultSelectedIndex = 2;
```
### 顶部轮播图手势处理
如果TableHeaderView添加了轮播图获取其他可以横向滚动的UIScrollView。如果不处理就会出现左右滚动轮播图的时候又可以触发整个页面的上下滚动。为了规避该问题请参考示例仓库中`BannerViewController`类的处理方法。即可同一时间只允许左右滚动或者上下滚动。
### 关于列表用UIViewController封装且要支持横竖屏的tips
在列表UIViewController类里面一定要加上下面这段代码(不要问我为什么,我也不知道,谁知道系统内部是怎么操作的,反正加上就没毛病了)
```
- (void)loadView {
self.view = [[UIView alloc] init];
}
```
### `JXPagerSmoothView` header有UITextField或者`UITextView`
详情参考OC版本示例【滚动延续 Header有输入框】
列表自定义子类化`UITableView`或者`UICollectionView`,然后重载`scrollRectToVisible`方法,示例代码如下。
```Object-C
@implementation TestTableView
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated {
[self setContentOffset:CGPointMake(self.contentOffset.x, rect.origin.y) animated:animated];
}
@end
```
### `FDFullscreenPopGesture`等全屏手势兼容处理
[全屏手势兼容处理文档,点击查看 ❗️❗️❗️](https://github.com/pujiaxin33/JXPagingView/blob/master/Document/%E5%85%A8%E5%B1%8F%E6%89%8B%E5%8A%BF%E5%A4%84%E7%90%86.md)
## 迁移指南
- **0.0.9版本**将下面两个API的返回值修改为了NSUInteger(swift版本为Int)之前版本是CGFloat升级为0.0.9及以上的时候记得修改一下使用地方的返回值类型不然会引起crash。
- `- (NSUInteger)heightForPinSectionHeaderInPagerView:(JXPagerView *)pagerView`
- `- (NSUInteger)tableHeaderViewHeightInPagerView:(JXPagerView *)pagerView`
- **1.0.0版本**
删除代理方法`- (NSArray <id<JXPagerViewListViewDelegate>> *)listViewsInPagerView:(JXPagerView *)pagerView;`,请参考示例使用下面两个代理方法:
- `- (NSInteger)numberOfListsInPagerView:(JXPagerView *)pagerView;`
- `- (id<JXPagerViewListViewDelegate>)pagerView:(JXPagerView *)pagerView initListAtIndex:(NSInteger)index;`
- **2.0.0版本**`JXPagerListContainerView`进行了重构,列表拥有了完整的生命周期方法。列表是`UIViewController`类,`viewWillAppear`等生命周期方法将会正确触发。
-
- 删除了collectionView用`scrollView`属性替换。
- 和`CategoryView`的联动绑定代码更新为`self.categoryView.listContainer = (id<JXCategoryViewListContainer>)self.pagerView.listContainerView;`。
- `JXPagerView`新增`- (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate listContainerType:(JXPagerListContainerType)type`初始化方法,可以指定列表容器为`UIScrollView`或者`UICollectionView`
## 补充
有不明白的地方建议多看下源码。再有疑问的欢迎提Issue交流🤝

View File

@@ -0,0 +1,128 @@
//
// JXCategoryListScrollView.h
// JXCategoryView
//
// Created by jiaxin on 2018/9/12.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
@class JXPagerListContainerView;
@class JXPagerListContainerScrollView;
@protocol JXPagerViewListViewDelegate <NSObject>
/**
返回listView。如果是vc包裹的就是vc.view如果是自定义view包裹的就是自定义view自己。
@return UIView
*/
- (UIView *)listView;
/**
返回listView内部持有的UIScrollView或UITableView或UICollectionView
主要用于mainTableView已经显示了headerlistView的contentOffset需要重置时内部需要访问到外部传入进来的listView内的scrollView
@return listView内部持有的UIScrollView或UITableView或UICollectionView
*/
- (UIScrollView *)listScrollView;
/**
当listView内部持有的UIScrollView或UITableView或UICollectionView的代理方法`scrollViewDidScroll`回调时需要调用该代理方法传入的callback
@param callback `scrollViewDidScroll`回调时调用的callback
*/
- (void)listViewDidScrollCallback:(void (^)(UIScrollView *scrollView))callback;
@optional
- (void)listScrollViewWillResetContentOffset;
- (void)listWillAppear;
- (void)listDidAppear;
- (void)listWillDisappear;
- (void)listDidDisappear;
@end
/**
列表容器视图的类型
- ScrollView: UIScrollView。优势没有其他副作用。劣势实时的视图内存占用相对大一点因为所有加载之后的列表视图都在视图层级里面。
- CollectionView: 使用UICollectionView。优势因为列表被添加到cell上实时的视图内存占用更少适合内存要求特别高的场景。劣势因为cell重用机制的问题导致列表被移除屏幕外之后会被放入缓存区而不存在于视图层级中。如果刚好你的列表使用了下拉刷新视图在快速切换过程中就会导致下拉刷新回调不成功的问题。一句话概括使用CollectionView的时候就不要让列表使用下拉刷新加载。
*/
typedef NS_ENUM(NSUInteger, JXPagerListContainerType) {
JXPagerListContainerType_ScrollView,
JXPagerListContainerType_CollectionView,
};
@protocol JXPagerListContainerViewDelegate <NSObject>
/**
返回list的数量
@param listContainerView 列表的容器视图
@return list的数量
*/
- (NSInteger)numberOfListsInlistContainerView:(JXPagerListContainerView *)listContainerView;
/**
根据index返回一个对应列表实例需要是遵从`JXPagerViewListViewDelegate`协议的对象。
你可以代理方法调用的时候初始化对应列表,达到懒加载的效果。这也是默认推荐的初始化列表方法。你也可以提前创建好列表,等该代理方法回调的时候再返回也可以,达到预加载的效果。
如果列表是用自定义UIView封装的就让自定义UIView遵从`JXPagerViewListViewDelegate`协议该方法返回自定义UIView即可。
如果列表是用自定义UIViewController封装的就让自定义UIViewController遵从`JXPagerViewListViewDelegate`协议该方法返回自定义UIViewController即可。
@param listContainerView 列表的容器视图
@param index 目标下标
@return 遵从JXPagerViewListViewDelegate协议的list实例
*/
- (id<JXPagerViewListViewDelegate>)listContainerView:(JXPagerListContainerView *)listContainerView initListForIndex:(NSInteger)index;
@optional
/**
返回自定义UIScrollView或UICollectionView的Class
某些特殊情况需要自己处理UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture需要处理手势相关代理。
@param listContainerView JXPagerListContainerView
@return 自定义UIScrollView实例
*/
- (Class)scrollViewClassInlistContainerView:(JXPagerListContainerView *)listContainerView;
/**
控制能否初始化对应index的列表。有些业务需求需要在某些情况才允许初始化某些列表通过通过该代理实现控制。
*/
- (BOOL)listContainerView:(JXPagerListContainerView *)listContainerView canInitListAtIndex:(NSInteger)index;
- (void)listContainerViewDidScroll:(UIScrollView *)scrollView;
- (void)listContainerViewWillBeginDragging:(JXPagerListContainerView *)listContainerView;
- (void)listContainerViewWDidEndScroll:(JXPagerListContainerView *)listContainerView;
- (void)listContainerView:(JXPagerListContainerView *)listContainerView listDidAppearAtIndex:(NSInteger)index;
@end
@interface JXPagerListContainerView : UIView
@property (nonatomic, assign, readonly) JXPagerListContainerType containerType;
@property (nonatomic, strong, readonly) UIScrollView *scrollView;
@property (nonatomic, strong, readonly) NSDictionary <NSNumber *, id<JXPagerViewListViewDelegate>> *validListDict; //已经加载过的列表字典。key是indexvalue是对应的列表
@property (nonatomic, strong) UIColor *listCellBackgroundColor; //默认:[UIColor whiteColor]
/**
滚动切换的时候滚动距离超过一页的多少百分比就触发列表的初始化。默认0.01即列表显示了一点就触发加载。范围0~1开区间不包括0和1
*/
@property (nonatomic, assign) CGFloat initListPercent;
///当使用Category嵌套Paging的时候需要设置为YES默认为NO
@property (nonatomic, assign, getter=isCategoryNestPagingEnabled) BOOL categoryNestPagingEnabled;
@property (nonatomic, assign, readonly) NSInteger currentIndex;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
- (instancetype)initWithType:(JXPagerListContainerType)type delegate:(id<JXPagerListContainerViewDelegate>)delegate NS_DESIGNATED_INITIALIZER;
@end
@interface JXPagerListContainerView (ListContainer)
- (void)setDefaultSelectedIndex:(NSInteger)index;
- (UIScrollView *)contentScrollView;
- (void)reloadData;
- (void)scrollingFromLeftIndex:(NSInteger)leftIndex toRightIndex:(NSInteger)rightIndex ratio:(CGFloat)ratio selectedIndex:(NSInteger)selectedIndex;
- (void)didClickSelectedItemAtIndex:(NSInteger)index;
@end

View File

@@ -0,0 +1,597 @@
//
// JXPagerListContainerView.m
// JXCategoryView
//
// Created by jiaxin on 2018/9/12.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXPagerListContainerView.h"
#import <objc/runtime.h>
@interface JXPagerListContainerScrollView: UIScrollView <UIGestureRecognizerDelegate>
@property (nonatomic, assign, getter=isCategoryNestPagingEnabled) BOOL categoryNestPagingEnabled;
@end
@implementation JXPagerListContainerScrollView
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if (self.isCategoryNestPagingEnabled) {
if ([gestureRecognizer isMemberOfClass:NSClassFromString(@"UIScrollViewPanGestureRecognizer")]) {
CGFloat velocityX = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:gestureRecognizer.view].x;
//x0
if (velocityX > 0) {
if (self.contentOffset.x == 0) {
return NO;
}
}else if (velocityX < 0) {
//x0
if (self.contentOffset.x + self.bounds.size.width == self.contentSize.width) {
return NO;
}
}
}
}
return YES;
}
@end
@interface JXPagerListContainerCollectionView: UICollectionView <UIGestureRecognizerDelegate>
@property (nonatomic, assign, getter=isCategoryNestPagingEnabled) BOOL categoryNestPagingEnabled;
@end
@implementation JXPagerListContainerCollectionView
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if (self.isCategoryNestPagingEnabled) {
if ([gestureRecognizer isMemberOfClass:NSClassFromString(@"UIScrollViewPanGestureRecognizer")]) {
CGFloat velocityX = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:gestureRecognizer.view].x;
//x0
if (velocityX > 0) {
if (self.contentOffset.x == 0) {
return NO;
}
}else if (velocityX < 0) {
//x0
if (self.contentOffset.x + self.bounds.size.width == self.contentSize.width) {
return NO;
}
}
}
}
return YES;
}
@end
@interface JXPagerListContainerViewController : UIViewController
@property (copy) void(^viewWillAppearBlock)(void);
@property (copy) void(^viewDidAppearBlock)(void);
@property (copy) void(^viewWillDisappearBlock)(void);
@property (copy) void(^viewDidDisappearBlock)(void);
@end
@implementation JXPagerListContainerViewController
- (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 JXPagerListContainerView () <UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, weak) id<JXPagerListContainerViewDelegate> delegate;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic, strong) NSMutableDictionary <NSNumber *, id<JXPagerViewListViewDelegate>> *validListDict;
@property (nonatomic, assign) NSInteger willAppearIndex;
@property (nonatomic, assign) NSInteger willDisappearIndex;
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) JXPagerListContainerViewController *containerVC;
@end
@implementation JXPagerListContainerView
- (instancetype)initWithType:(JXPagerListContainerType)type delegate:(id<JXPagerListContainerViewDelegate>)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 = [[JXPagerListContainerViewController 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 == JXPagerListContainerType_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 = [[JXPagerListContainerScrollView alloc] init];
}
self.scrollView.backgroundColor = [UIColor clearColor];
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, *)) {
self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
[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 = [[JXPagerListContainerCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
}
self.collectionView.backgroundColor = [UIColor clearColor];
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, *)) {
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
[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 == JXPagerListContainerType_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<JXPagerViewListViewDelegate> _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);
}];
self.scrollView.contentOffset = CGPointMake(self.currentIndex*self.scrollView.bounds.size.width, 0);
}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.frame = self.bounds;
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
[self.collectionView setContentOffset:CGPointMake(self.collectionView.bounds.size.width*self.currentIndex, 0) 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)setCategoryNestPagingEnabled:(BOOL)categoryNestPagingEnabled {
_categoryNestPagingEnabled = categoryNestPagingEnabled;
if ([self.scrollView isKindOfClass:[JXPagerListContainerScrollView class]]) {
((JXPagerListContainerScrollView *)self.scrollView).categoryNestPagingEnabled = categoryNestPagingEnabled;
}else if ([self.scrollView isKindOfClass:[JXPagerListContainerCollectionView class]]) {
((JXPagerListContainerCollectionView *)self.scrollView).categoryNestPagingEnabled = categoryNestPagingEnabled;
}
}
#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;
for (UIView *subview in cell.contentView.subviews) {
[subview removeFromSuperview];
}
id<JXPagerViewListViewDelegate> 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
if ([list isKindOfClass:[UIViewController class]]) {
[list listView].frame = cell.contentView.bounds;
} else {
[list listView].frame = cell.bounds;
}
[cell.contentView addSubview:[list listView]];
}
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(listContainerViewWDidEndScroll:)]) {
[self.delegate listContainerViewWDidEndScroll:self];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerViewWillBeginDragging:)]) {
[self.delegate listContainerViewWillBeginDragging:self];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerViewWDidEndScroll:)]) {
[self.delegate listContainerViewWDidEndScroll:self];
}
}
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerViewWDidEndScroll:)]) {
[self.delegate listContainerViewWDidEndScroll:self];
}
}
#pragma mark - JXCategoryViewListContainer
- (UIScrollView *)contentScrollView {
return self.scrollView;
}
- (void)setDefaultSelectedIndex:(NSInteger)index {
self.currentIndex = index;
}
- (void)scrollingFromLeftIndex:(NSInteger)leftIndex toRightIndex:(NSInteger)rightIndex ratio:(CGFloat)ratio selectedIndex:(NSInteger)selectedIndex {
}
- (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<JXPagerViewListViewDelegate> list in _validListDict.allValues) {
[[list listView] removeFromSuperview];
if ([list isKindOfClass:[UIViewController class]]) {
[(UIViewController *)list removeFromParentViewController];
}
}
[_validListDict removeAllObjects];
if (self.containerType == JXPagerListContainerType_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<JXPagerViewListViewDelegate> 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;
switch (self.containerType) {
case JXPagerListContainerType_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]];
break;
}
case JXPagerListContainerType_CollectionView: {
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
if (cell != nil) {
for (UIView *subview in cell.contentView.subviews) {
[subview removeFromSuperview];
}
[list listView].frame = cell.contentView.bounds;
[cell.contentView addSubview:[list listView]];
}
break;
}
}
}
- (void)listWillAppear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
id<JXPagerViewListViewDelegate> list = _validListDict[@(index)];
if (list != nil) {
if (list && [list respondsToSelector:@selector(listWillAppear)]) {
[list listWillAppear];
}
if ([list isKindOfClass:[UIViewController class]]) {
UIViewController *listVC = (UIViewController *)list;
[listVC beginAppearanceTransition:YES animated:NO];
}
}else {
//listWillAppear
BOOL canInitList = YES;
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerView:canInitListAtIndex:)]) {
canInitList = [self.delegate listContainerView:self canInitListAtIndex:index];
}
if (canInitList) {
id<JXPagerViewListViewDelegate> list = _validListDict[@(index)];
if (list == nil) {
list = [self.delegate listContainerView:self initListForIndex:index];
if ([list isKindOfClass:[UIViewController class]]) {
[self.containerVC addChildViewController:(UIViewController *)list];
}
_validListDict[@(index)] = list;
}
if (self.containerType == JXPagerListContainerType_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]];
if (list && [list respondsToSelector:@selector(listWillAppear)]) {
[list listWillAppear];
}
if ([list isKindOfClass:[UIViewController class]]) {
UIViewController *listVC = (UIViewController *)list;
[listVC beginAppearanceTransition:YES animated:NO];
}
}
}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<JXPagerViewListViewDelegate> list = _validListDict[@(index)];
if (list && [list respondsToSelector:@selector(listDidAppear)]) {
[list listDidAppear];
}
if ([list isKindOfClass:[UIViewController class]]) {
UIViewController *listVC = (UIViewController *)list;
[listVC endAppearanceTransition];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(listContainerView:listDidAppearAtIndex:)]) {
[self.delegate listContainerView:self listDidAppearAtIndex:index];
}
}
- (void)listWillDisappear:(NSInteger)index {
if (![self checkIndexValid:index]) {
return;
}
id<JXPagerViewListViewDelegate> 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<JXPagerViewListViewDelegate> 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,14 @@
//
// JXPagingListRefreshView.h
// JXPagingView
//
// Created by jiaxin on 2018/8/28.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "JXPagerView.h"
@interface JXPagerListRefreshView : JXPagerView
@end

View File

@@ -0,0 +1,109 @@
//
// JXPagerListRefreshView.m
// JXPagerView
//
// Created by jiaxin on 2018/8/28.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXPagerListRefreshView.h"
@interface JXPagerListRefreshView()
@property (nonatomic, assign) CGFloat lastScrollingListViewContentOffsetY;
@end
@implementation JXPagerListRefreshView
- (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate listContainerType:(JXPagerListContainerType)type {
self = [super initWithDelegate:delegate listContainerType:type];
if (self) {
self.mainTableView.bounces = NO;
}
return self;
}
- (void)preferredProcessListViewDidScroll:(UIScrollView *)scrollView {
BOOL shouldProcess = YES;
if (self.currentScrollingListView.contentOffset.y > self.lastScrollingListViewContentOffsetY) {
//
}else {
//
if (self.mainTableView.contentOffset.y == 0) {
shouldProcess = NO;
}else {
if (self.mainTableView.contentOffset.y < self.mainTableViewMaxContentOffsetY) {
//mainTableViewheaderlistScrollView0
if (self.currentList && [self.currentList respondsToSelector:@selector(listScrollViewWillResetContentOffset)]) {
[self.currentList listScrollViewWillResetContentOffset];
}
[self setListScrollViewToMinContentOffsetY:self.currentScrollingListView];
if (self.automaticallyDisplayListVerticalScrollIndicator) {
self.currentScrollingListView.showsVerticalScrollIndicator = NO;
}
}
}
}
if (shouldProcess) {
if (self.mainTableView.contentOffset.y < self.mainTableViewMaxContentOffsetY) {
//scrollView.contentOffset.y0
if (self.currentScrollingListView.contentOffset.y > [self minContentOffsetYInListScrollView:self.currentScrollingListView]) {
//mainTableViewheaderlistScrollView0
if (self.currentList && [self.currentList respondsToSelector:@selector(listScrollViewWillResetContentOffset)]) {
[self.currentList listScrollViewWillResetContentOffset];
}
[self setListScrollViewToMinContentOffsetY:self.currentScrollingListView];
if (self.automaticallyDisplayListVerticalScrollIndicator) {
self.currentScrollingListView.showsVerticalScrollIndicator = NO;
}
}
} else {
//mainTableViewheadermainTableViewlistScrollView
self.mainTableView.contentOffset = CGPointMake(0, self.mainTableViewMaxContentOffsetY);
if (self.automaticallyDisplayListVerticalScrollIndicator) {
self.currentScrollingListView.showsVerticalScrollIndicator = YES;
}
}
}
self.lastScrollingListViewContentOffsetY = self.currentScrollingListView.contentOffset.y;
}
- (void)preferredProcessMainTableViewDidScroll:(UIScrollView *)scrollView {
if (self.pinSectionHeaderVerticalOffset != 0) {
if (!(self.currentScrollingListView != nil && self.currentScrollingListView.contentOffset.y > [self minContentOffsetYInListScrollView:self.currentScrollingListView])) {
//listView
if (scrollView.contentOffset.y <= 0) {
self.mainTableView.bounces = NO;
self.mainTableView.contentOffset = CGPointZero;
return;
}else {
self.mainTableView.bounces = YES;
}
}
}
if (self.currentScrollingListView != nil && self.currentScrollingListView.contentOffset.y > [self minContentOffsetYInListScrollView:self.currentScrollingListView]) {
//mainTableViewheaderlistViewmainTableViewcontentOffset
[self setMainTableViewToMaxContentOffsetY];
}
if (scrollView.contentOffset.y < self.mainTableViewMaxContentOffsetY) {
//mainTableViewheaderlistViewcontentOffset
for (id<JXPagerViewListViewDelegate> list in self.validListDict.allValues) {
//
UIScrollView *listScrollView = [list listScrollView];
if (listScrollView.contentOffset.y > 0) {
if ([list respondsToSelector:@selector(listScrollViewWillResetContentOffset)]) {
[list listScrollViewWillResetContentOffset];
}
[self setListScrollViewToMinContentOffsetY:listScrollView];
}
}
}
if (scrollView.contentOffset.y > self.mainTableViewMaxContentOffsetY && self.currentScrollingListView.contentOffset.y == [self minContentOffsetYInListScrollView:self.currentScrollingListView]) {
//mainTableViewheaderViewlistView
[self setMainTableViewToMaxContentOffsetY];
}
}
@end

View File

@@ -0,0 +1,19 @@
//
// JXPagingMainTableView.h
// JXPagingView
//
// Created by jiaxin on 2018/8/27.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol JXPagerMainTableViewGestureDelegate <NSObject>
- (BOOL)mainTableViewGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
@end
@interface JXPagerMainTableView : UITableView
@property (nonatomic, weak) id<JXPagerMainTableViewGestureDelegate> gestureDelegate;
@end

View File

@@ -0,0 +1,25 @@
//
// JXPagerMainTableView.m
// JXPagerView
//
// Created by jiaxin on 2018/8/27.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXPagerMainTableView.h"
@interface JXPagerMainTableView ()<UIGestureRecognizerDelegate>
@end
@implementation JXPagerMainTableView
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if (self.gestureDelegate && [self.gestureDelegate respondsToSelector:@selector(mainTableViewGestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]) {
return [self.gestureDelegate mainTableViewGestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
}else {
return [gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]];
}
}
@end

View File

@@ -0,0 +1,91 @@
//
// JXPagerSmoothView.h
// JXPagerViewExample-OC
//
// Created by jiaxin on 2019/11/15.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
@class JXPagerSmoothView;
@protocol JXPagerSmoothViewListViewDelegate <NSObject>
/**
返回listView。如果是vc包裹的就是vc.view如果是自定义view包裹的就是自定义view自己。
*/
- (UIView *)listView;
/**
返回JXPagerSmoothViewListViewDelegate内部持有的UIScrollView或UITableView或UICollectionView
*/
- (UIScrollView *)listScrollView;
@optional
- (void)listDidAppear;
- (void)listDidDisappear;
@end
@protocol JXPagerSmoothViewDataSource <NSObject>
/**
返回页面header的高度
*/
- (CGFloat)heightForPagerHeaderInPagerView:(JXPagerSmoothView *)pagerView;
/**
返回页面header视图
*/
- (UIView *)viewForPagerHeaderInPagerView:(JXPagerSmoothView *)pagerView;
/**
返回悬浮视图的高度
*/
- (CGFloat)heightForPinHeaderInPagerView:(JXPagerSmoothView *)pagerView;
/**
返回悬浮视图
*/
- (UIView *)viewForPinHeaderInPagerView:(JXPagerSmoothView *)pagerView;
/**
返回列表的数量
*/
- (NSInteger)numberOfListsInPagerView:(JXPagerSmoothView *)pagerView;
/**
根据index初始化一个对应列表实例需要是遵从`JXPagerSmoothViewListViewDelegate`协议的对象。
如果列表是用自定义UIView封装的就让自定义UIView遵从`JXPagerSmoothViewListViewDelegate`协议该方法返回自定义UIView即可。
如果列表是用自定义UIViewController封装的就让自定义UIViewController遵从`JXPagerSmoothViewListViewDelegate`协议该方法返回自定义UIViewController即可。
@param pagerView pagerView description
@param index index description
@return 新生成的列表实例
*/
- (id<JXPagerSmoothViewListViewDelegate>)pagerView:(JXPagerSmoothView *)pagerView initListAtIndex:(NSInteger)index;
@end
@protocol JXPagerSmoothViewDelegate <NSObject>
- (void)pagerSmoothViewDidScroll:(UIScrollView *)scrollView;
@end
@interface JXPagerSmoothView : UIView
/**
当前已经加载过的列表key就是@(index)值value是对应的列表。
*/
@property (nonatomic, strong, readonly) NSDictionary <NSNumber *, id<JXPagerSmoothViewListViewDelegate>> *listDict;
@property (nonatomic, strong, readonly) UICollectionView *listCollectionView;
@property (nonatomic, assign) NSInteger defaultSelectedIndex;
@property (nonatomic, weak) id<JXPagerSmoothViewDelegate> delegate;
- (instancetype)initWithDataSource:(id<JXPagerSmoothViewDataSource>)dataSource NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
- (void)reloadData;
@end

View File

@@ -0,0 +1,361 @@
//
// JXPagerSmoothView.m
// JXPagerViewExample-OC
//
// Created by jiaxin on 2019/11/15.
// Copyright © 2019 jiaxin. All rights reserved.
//
#import "JXPagerSmoothView.h"
static NSString *JXPagerSmoothViewCollectionViewCellIdentifier = @"cell";
@interface JXPagerSmoothCollectionView : UICollectionView <UIGestureRecognizerDelegate>
@property (nonatomic, strong) UIView *pagerHeaderContainerView;
@end
@implementation JXPagerSmoothCollectionView
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
CGPoint point = [touch locationInView:self.pagerHeaderContainerView];
if (CGRectContainsPoint(self.pagerHeaderContainerView.bounds, point)) {
return NO;
}
return YES;
}
@end
@interface JXPagerSmoothView () <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, weak) id<JXPagerSmoothViewDataSource> dataSource;
@property (nonatomic, strong) JXPagerSmoothCollectionView *listCollectionView;
@property (nonatomic, strong) NSMutableDictionary <NSNumber *, id<JXPagerSmoothViewListViewDelegate>> *listDict;
@property (nonatomic, strong) NSMutableDictionary <NSNumber *, UIView*> *listHeaderDict;
@property (nonatomic, assign, getter=isSyncListContentOffsetEnabled) BOOL syncListContentOffsetEnabled;
@property (nonatomic, strong) UIView *pagerHeaderContainerView;
@property (nonatomic, assign) CGFloat currentPagerHeaderContainerViewY;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic, strong) UIScrollView *currentListScrollView;
@property (nonatomic, assign) CGFloat heightForPagerHeader;
@property (nonatomic, assign) CGFloat heightForPinHeader;
@property (nonatomic, assign) CGFloat heightForPagerHeaderContainerView;
@property (nonatomic, assign) CGFloat currentListInitializeContentOffsetY;
@property (nonatomic, strong) UIScrollView *singleScrollView;
@end
@implementation JXPagerSmoothView
- (void)dealloc
{
for (id<JXPagerSmoothViewListViewDelegate> list in self.listDict.allValues) {
[[list listScrollView] removeObserver:self forKeyPath:@"contentOffset"];
[[list listScrollView] removeObserver:self forKeyPath:@"contentSize"];
}
}
- (instancetype)initWithDataSource:(id<JXPagerSmoothViewDataSource>)dataSource
{
self = [super initWithFrame:CGRectZero];
if (self) {
_dataSource = dataSource;
_listDict = [NSMutableDictionary dictionary];
_listHeaderDict = [NSMutableDictionary dictionary];
[self initializeViews];
}
return self;
}
- (void)initializeViews {
self.pagerHeaderContainerView = [[UIView alloc] init];
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.minimumLineSpacing = 0;
layout.minimumInteritemSpacing = 0;
_listCollectionView = [[JXPagerSmoothCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
self.listCollectionView.dataSource = self;
self.listCollectionView.delegate = self;
self.listCollectionView.pagingEnabled = YES;
self.listCollectionView.bounces = NO;
self.listCollectionView.showsHorizontalScrollIndicator = NO;
self.listCollectionView.scrollsToTop = NO;
[self.listCollectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:JXPagerSmoothViewCollectionViewCellIdentifier];
if (@available(iOS 10.0, *)) {
self.listCollectionView.prefetchingEnabled = NO;
}
if (@available(iOS 11.0, *)) {
self.listCollectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
_listCollectionView.pagerHeaderContainerView = self.pagerHeaderContainerView;
[self addSubview:self.listCollectionView];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.listCollectionView.frame = self.bounds;
if (CGRectEqualToRect(self.pagerHeaderContainerView.frame, CGRectZero)) {
[self reloadData];
}
if (self.singleScrollView != nil) {
self.singleScrollView.frame = self.bounds;
}
}
- (void)reloadData {
self.currentListScrollView = nil;
self.currentIndex = self.defaultSelectedIndex;
self.currentPagerHeaderContainerViewY = 0;
self.syncListContentOffsetEnabled = NO;
[self.listHeaderDict removeAllObjects];
for (id<JXPagerSmoothViewListViewDelegate> list in self.listDict.allValues) {
[[list listScrollView] removeObserver:self forKeyPath:@"contentOffset"];
[[list listScrollView] removeObserver:self forKeyPath:@"contentSize"];
[[list listView] removeFromSuperview];
}
[_listDict removeAllObjects];
self.heightForPagerHeader = [self.dataSource heightForPagerHeaderInPagerView:self];
self.heightForPinHeader = [self.dataSource heightForPinHeaderInPagerView:self];
self.heightForPagerHeaderContainerView = self.heightForPagerHeader + self.heightForPinHeader;
UIView *pagerHeader = [self.dataSource viewForPagerHeaderInPagerView:self];
UIView *pinHeader = [self.dataSource viewForPinHeaderInPagerView:self];
[self.pagerHeaderContainerView addSubview:pagerHeader];
[self.pagerHeaderContainerView addSubview:pinHeader];
self.pagerHeaderContainerView.frame = CGRectMake(0, 0, self.bounds.size.width, self.heightForPagerHeaderContainerView);
pagerHeader.frame = CGRectMake(0, 0, self.bounds.size.width, self.heightForPagerHeader);
pinHeader.frame = CGRectMake(0, self.heightForPagerHeader, self.bounds.size.width, self.heightForPinHeader);
[self.listCollectionView setContentOffset:CGPointMake(self.listCollectionView.bounds.size.width*self.defaultSelectedIndex, 0) animated:NO];
[self.listCollectionView reloadData];
if ([self.dataSource numberOfListsInPagerView:self] == 0) {
self.singleScrollView = [[UIScrollView alloc] init];
[self addSubview:self.singleScrollView];
[self.singleScrollView addSubview:pagerHeader];
self.singleScrollView.contentSize = CGSizeMake(self.bounds.size.width, self.heightForPagerHeader);
}else if (self.singleScrollView != nil) {
[self.singleScrollView removeFromSuperview];
self.singleScrollView = nil;
}
}
#pragma mark - UICollectionViewDataSource & UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return self.bounds.size;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return [self.dataSource numberOfListsInPagerView:self];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:JXPagerSmoothViewCollectionViewCellIdentifier forIndexPath:indexPath];
id<JXPagerSmoothViewListViewDelegate> list = self.listDict[@(indexPath.item)];
if (list == nil) {
list = [self.dataSource pagerView:self initListAtIndex:indexPath.item];
_listDict[@(indexPath.item)] = list;
[[list listView] setNeedsLayout];
[[list listView] layoutIfNeeded];
UIScrollView *listScrollView = [list listScrollView];
if ([listScrollView isKindOfClass:[UITableView class]]) {
((UITableView *)listScrollView).estimatedRowHeight = 0;
((UITableView *)listScrollView).estimatedSectionFooterHeight = 0;
((UITableView *)listScrollView).estimatedSectionHeaderHeight = 0;
}
if (@available(iOS 11.0, *)) {
listScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
listScrollView.contentInset = UIEdgeInsetsMake(self.heightForPagerHeaderContainerView, 0, 0, 0);
self.currentListInitializeContentOffsetY = -listScrollView.contentInset.top + MIN(-self.currentPagerHeaderContainerViewY, self.heightForPagerHeader);
listScrollView.contentOffset = CGPointMake(0, self.currentListInitializeContentOffsetY);
UIView *listHeader = [[UIView alloc] initWithFrame:CGRectMake(0, -self.heightForPagerHeaderContainerView, self.bounds.size.width, self.heightForPagerHeaderContainerView)];
[listScrollView addSubview:listHeader];
if (self.pagerHeaderContainerView.superview == nil) {
[listHeader addSubview:self.pagerHeaderContainerView];
}
self.listHeaderDict[@(indexPath.item)] = listHeader;
[listScrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
[listScrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
}
for (id<JXPagerSmoothViewListViewDelegate> listItem in self.listDict.allValues) {
[listItem listScrollView].scrollsToTop = (listItem == list);
}
UIView *listView = [list listView];
if (listView != nil && listView.superview != cell.contentView) {
for (UIView *view in cell.contentView.subviews) {
[view removeFromSuperview];
}
listView.frame = cell.contentView.bounds;
[cell.contentView addSubview:listView];
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
[self listDidAppear:indexPath.item];
}
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
[self listDidDisappear:indexPath.item];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (self.delegate && [self.delegate respondsToSelector:@selector(pagerSmoothViewDidScroll:)]) {
[self.delegate pagerSmoothViewDidScroll:scrollView];
}
CGFloat indexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width;
NSInteger index = floor(indexPercent);
UIScrollView *listScrollView = [self.listDict[@(index)] listScrollView];
if (indexPercent - index == 0 && index != self.currentIndex && !(scrollView.isDragging || scrollView.isDecelerating) && listScrollView.contentOffset.y <= -self.heightForPinHeader) {
[self horizontalScrollDidEndAtIndex:index];
}else {
//listHeaderContainerViewself
if (self.pagerHeaderContainerView.superview != self) {
self.pagerHeaderContainerView.frame = CGRectMake(0, self.currentPagerHeaderContainerViewY, self.pagerHeaderContainerView.bounds.size.width, self.pagerHeaderContainerView.bounds.size.height);
[self addSubview:self.pagerHeaderContainerView];
}
}
if (index != self.currentIndex) {
self.currentIndex = index;
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
NSInteger index = scrollView.contentOffset.x/scrollView.bounds.size.width;
[self horizontalScrollDidEndAtIndex:index];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
NSInteger index = scrollView.contentOffset.x/scrollView.bounds.size.width;
[self horizontalScrollDidEndAtIndex:index];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"]) {
UIScrollView *scrollView = (UIScrollView *)object;
if (scrollView != nil) {
[self listDidScroll:scrollView];
}
}else if([keyPath isEqualToString:@"contentSize"]) {
UIScrollView *scrollView = (UIScrollView *)object;
if (scrollView != nil) {
CGFloat minContentSizeHeight = self.bounds.size.height - self.heightForPinHeader;
if (minContentSizeHeight > scrollView.contentSize.height) {
scrollView.contentSize = CGSizeMake(scrollView.contentSize.width, minContentSizeHeight);
//scrollViewcontentOffset
if (_currentListScrollView != nil && scrollView != _currentListScrollView) {
scrollView.contentOffset = CGPointMake(0, self.currentListInitializeContentOffsetY);
}
}
}
}else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - Event
- (void)listDidScroll:(UIScrollView *)scrollView {
if (self.listCollectionView.isDragging || self.listCollectionView.isDecelerating) {
return;
}
NSInteger listIndex = [self listIndexForListScrollView:scrollView];
if (listIndex != self.currentIndex) {
return;
}
self.currentListScrollView = scrollView;
CGFloat contentOffsetY = scrollView.contentOffset.y + self.heightForPagerHeaderContainerView;
if (contentOffsetY < self.heightForPagerHeader) {
self.syncListContentOffsetEnabled = YES;
self.currentPagerHeaderContainerViewY = -contentOffsetY;
for (id<JXPagerSmoothViewListViewDelegate> list in self.listDict.allValues) {
if ([list listScrollView] != self.currentListScrollView) {
[[list listScrollView] setContentOffset:scrollView.contentOffset animated:NO];
}
}
UIView *listHeader = [self listHeaderForListScrollView:scrollView];
if (self.pagerHeaderContainerView.superview != listHeader) {
self.pagerHeaderContainerView.frame = CGRectMake(0, 0, self.pagerHeaderContainerView.bounds.size.width, self.pagerHeaderContainerView.bounds.size.height);
[listHeader addSubview:self.pagerHeaderContainerView];
}
}else {
if (self.pagerHeaderContainerView.superview != self) {
self.pagerHeaderContainerView.frame = CGRectMake(0, -self.heightForPagerHeader, self.pagerHeaderContainerView.bounds.size.width, self.pagerHeaderContainerView.bounds.size.height);
[self addSubview:self.pagerHeaderContainerView];
}
if (self.isSyncListContentOffsetEnabled) {
self.syncListContentOffsetEnabled = NO;
self.currentPagerHeaderContainerViewY = -self.heightForPagerHeader;
for (id<JXPagerSmoothViewListViewDelegate> list in self.listDict.allValues) {
if ([list listScrollView] != scrollView) {
[[list listScrollView] setContentOffset:CGPointMake(0, -self.heightForPinHeader) animated:NO];
}
}
}
}
}
#pragma mark - Private
- (UIView *)listHeaderForListScrollView:(UIScrollView *)scrollView {
for (NSNumber *index in self.listDict) {
if ([self.listDict[index] listScrollView] == scrollView) {
return self.listHeaderDict[index];
}
}
return nil;
}
- (NSInteger)listIndexForListScrollView:(UIScrollView *)scrollView {
for (NSNumber *index in self.listDict) {
if ([self.listDict[index] listScrollView] == scrollView) {
return [index integerValue];
}
}
return 0;
}
- (void)listDidAppear:(NSInteger)index {
NSUInteger count = [self.dataSource numberOfListsInPagerView:self];
if (count <= 0 || index >= count) {
return;
}
id<JXPagerSmoothViewListViewDelegate> list = self.listDict[@(index)];
if (list && [list respondsToSelector:@selector(listDidAppear)]) {
[list listDidAppear];
}
}
- (void)listDidDisappear:(NSInteger)index {
NSUInteger count = [self.dataSource numberOfListsInPagerView:self];
if (count <= 0 || index >= count) {
return;
}
id<JXPagerSmoothViewListViewDelegate> list = self.listDict[@(index)];
if (list && [list respondsToSelector:@selector(listDidDisappear)]) {
[list listDidDisappear];
}
}
/// pagerHeaderContainerViewindex
- (void)horizontalScrollDidEndAtIndex:(NSInteger)index {
self.currentIndex = index;
UIView *listHeader = self.listHeaderDict[@(index)];
UIScrollView *listScrollView = [self.listDict[@(index)] listScrollView];
if (listHeader != nil && listScrollView.contentOffset.y <= -self.heightForPinHeader) {
for (id<JXPagerSmoothViewListViewDelegate> listItem in self.listDict.allValues) {
[listItem listScrollView].scrollsToTop = ([listItem listScrollView] == listScrollView);
}
self.pagerHeaderContainerView.frame = CGRectMake(0, 0, self.pagerHeaderContainerView.bounds.size.width, self.pagerHeaderContainerView.bounds.size.height);
[listHeader addSubview:self.pagerHeaderContainerView];
}
}
@end

View File

@@ -0,0 +1,131 @@
//
// JXPagerView.h
// JXPagerView
//
// Created by jiaxin on 2018/8/27.
// Copyright © 2018年 jiaxin. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "JXPagerMainTableView.h"
#import "JXPagerListContainerView.h"
@class JXPagerView;
@protocol JXPagerViewDelegate <NSObject>
/**
返回tableHeaderView的高度因为内部需要比对判断只能是整型数
*/
- (NSUInteger)tableHeaderViewHeightInPagerView:(JXPagerView *)pagerView;
/**
返回tableHeaderView
*/
- (UIView *)tableHeaderViewInPagerView:(JXPagerView *)pagerView;
/**
返回悬浮HeaderView的高度因为内部需要比对判断只能是整型数
*/
- (NSUInteger)heightForPinSectionHeaderInPagerView:(JXPagerView *)pagerView;
/**
返回悬浮HeaderView。我用的是自己封装的JXCategoryViewGithub:https://github.com/pujiaxin33/JXCategoryView你也可以选择其他的三方库或者自己写
*/
- (UIView *)viewForPinSectionHeaderInPagerView:(JXPagerView *)pagerView;
/**
返回列表的数量
*/
- (NSInteger)numberOfListsInPagerView:(JXPagerView *)pagerView;
/**
根据index初始化一个对应列表实例需要是遵从`JXPagerViewListViewDelegate`协议的对象。
如果列表是用自定义UIView封装的就让自定义UIView遵从`JXPagerViewListViewDelegate`协议该方法返回自定义UIView即可。
如果列表是用自定义UIViewController封装的就让自定义UIViewController遵从`JXPagerViewListViewDelegate`协议该方法返回自定义UIViewController即可。
注意:一定要是新生成的实例!!!
@param pagerView pagerView description
@param index index description
@return 新生成的列表实例
*/
- (id<JXPagerViewListViewDelegate>)pagerView:(JXPagerView *)pagerView initListAtIndex:(NSInteger)index;
@optional
/// 返回对应index的列表唯一标识
/// @param pagerView pagerView description
/// @param index index description
- (NSString *)pagerView:(JXPagerView *)pagerView listIdentifierAtIndex:(NSInteger)index;
- (void)mainTableViewDidScroll:(UIScrollView *)scrollView __attribute__ ((deprecated));
- (void)pagerView:(JXPagerView *)pagerView mainTableViewDidScroll:(UIScrollView *)scrollView;
- (void)pagerView:(JXPagerView *)pagerView mainTableViewWillBeginDragging:(UIScrollView *)scrollView;
- (void)pagerView:(JXPagerView *)pagerView mainTableViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)pagerView:(JXPagerView *)pagerView mainTableViewDidEndDecelerating:(UIScrollView *)scrollView;
- (void)pagerView:(JXPagerView *)pagerView mainTableViewDidEndScrollingAnimation:(UIScrollView *)scrollView;
/**
返回自定义UIScrollView或UICollectionView的Class
某些特殊情况需要自己处理列表容器内UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture需要处理手势相关代理。
@param pagerView JXPagerView
@return 自定义UIScrollView实例
*/
- (Class)scrollViewClassInlistContainerViewInPagerView:(JXPagerView *)pagerView;
@end
@interface JXPagerView : UIView
/**
需要和self.categoryView.defaultSelectedIndex保持一致
*/
@property (nonatomic, assign) NSInteger defaultSelectedIndex;
@property (nonatomic, strong, readonly) JXPagerMainTableView *mainTableView;
@property (nonatomic, strong, readonly) JXPagerListContainerView *listContainerView;
/**
当前已经加载过可用的列表字典key就是index值value是对应的列表。
*/
@property (nonatomic, strong, readonly) NSDictionary <NSNumber *, id<JXPagerViewListViewDelegate>> *validListDict;
/**
顶部固定sectionHeader的垂直偏移量。数值越大越往下沉。
*/
@property (nonatomic, assign) NSInteger pinSectionHeaderVerticalOffset;
/**
是否允许列表左右滑动。默认YES
*/
@property (nonatomic, assign) BOOL isListHorizontalScrollEnabled;
/**
是否允许当前列表自动显示或隐藏列表是垂直滚动指示器。YES悬浮的headerView滚动到顶部开始滚动列表时就会显示反之隐藏。NO内部不会处理列表的垂直滚动指示器。默认为YES。
*/
@property (nonatomic, assign) BOOL automaticallyDisplayListVerticalScrollIndicator;
/**
当allowsCacheList为true时请务必实现代理方法`- (NSString *)pagerView:(JXPagerView *)pagerView listIdentifierAtIndex:(NSInteger)index`
*/
@property (nonatomic, assign) BOOL allowsCacheList;
- (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate;
- (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate listContainerType:(JXPagerListContainerType)type NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
- (void)reloadData;
- (void)resizeTableHeaderViewHeightWithAnimatable:(BOOL)animatable duration:(NSTimeInterval)duration curve:(UIViewAnimationCurve)curve;
@end
/**
暴露给子类使用,请勿直接使用相关属性和方法!
*/
@interface JXPagerView (UISubclassingGet)
@property (nonatomic, strong, readonly) UIScrollView *currentScrollingListView;
@property (nonatomic, strong, readonly) id<JXPagerViewListViewDelegate> currentList;
@property (nonatomic, assign, readonly) CGFloat mainTableViewMaxContentOffsetY;
@end
@interface JXPagerView (UISubclassingHooks)
- (void)preferredProcessListViewDidScroll:(UIScrollView *)scrollView;
- (void)preferredProcessMainTableViewDidScroll:(UIScrollView *)scrollView;
- (void)setMainTableViewToMaxContentOffsetY;
- (void)setListScrollViewToMinContentOffsetY:(UIScrollView *)scrollView;
- (CGFloat)minContentOffsetYInListScrollView:(UIScrollView *)scrollView;
@end

View File

@@ -0,0 +1,409 @@
//
// JXPagerView.m
// JXPagerView
//
// Created by jiaxin on 2018/8/27.
// Copyright © 2018 jiaxin. All rights reserved.
//
#import "JXPagerView.h"
@class JXPagerListContainerScrollView;
@class JXPagerListContainerCollectionView;
@interface JXPagerView () <UITableViewDataSource, UITableViewDelegate, JXPagerListContainerViewDelegate>
@property (nonatomic, weak) id<JXPagerViewDelegate> delegate;
@property (nonatomic, strong) JXPagerMainTableView *mainTableView;
@property (nonatomic, strong) JXPagerListContainerView *listContainerView;
@property (nonatomic, strong) UIScrollView *currentScrollingListView;
@property (nonatomic, strong) id<JXPagerViewListViewDelegate> currentList;
@property (nonatomic, strong) NSMutableDictionary <NSNumber *, id<JXPagerViewListViewDelegate>> *validListDict;
@property (nonatomic, strong) UIView *tableHeaderContainerView;
@property (nonatomic, strong) NSMutableDictionary<NSString *, id<JXPagerViewListViewDelegate>> *listCache;
@end
@implementation JXPagerView
- (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate {
return [self initWithDelegate:delegate listContainerType:JXPagerListContainerType_CollectionView];
}
- (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate listContainerType:(JXPagerListContainerType)type {
self = [super initWithFrame:CGRectZero];
if (self) {
_delegate = delegate;
_validListDict = [NSMutableDictionary dictionary];
_automaticallyDisplayListVerticalScrollIndicator = YES;
_isListHorizontalScrollEnabled = YES;
_mainTableView = [[JXPagerMainTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self.mainTableView.showsVerticalScrollIndicator = NO;
self.mainTableView.showsHorizontalScrollIndicator = NO;
self.mainTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.mainTableView.scrollsToTop = NO;
self.mainTableView.dataSource = self;
self.mainTableView.delegate = self;
[self refreshTableHeaderView];
[self.mainTableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
if (@available(iOS 11.0, *)) {
self.mainTableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
if (@available(iOS 15.0, *)) {
self.mainTableView.sectionHeaderTopPadding = 0;
}
#endif
[self addSubview:self.mainTableView];
_listContainerView = [[JXPagerListContainerView alloc] initWithType:type delegate:self];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGRectEqualToRect(self.bounds, self.mainTableView.frame)) {
self.mainTableView.frame = self.bounds;
[self.mainTableView reloadData];
}
}
- (void)setDefaultSelectedIndex:(NSInteger)defaultSelectedIndex {
_defaultSelectedIndex = defaultSelectedIndex;
self.listContainerView.defaultSelectedIndex = defaultSelectedIndex;
}
- (void)setIsListHorizontalScrollEnabled:(BOOL)isListHorizontalScrollEnabled {
_isListHorizontalScrollEnabled = isListHorizontalScrollEnabled;
self.listContainerView.scrollView.scrollEnabled = isListHorizontalScrollEnabled;
}
- (void)reloadData {
self.currentList = nil;
self.currentScrollingListView = nil;
[_validListDict removeAllObjects];
//list
if (self.allowsCacheList) {
NSMutableArray *newListIdentifierArray = [NSMutableArray array];
if (self.delegate && [self.delegate respondsToSelector:@selector(numberOfListsInPagerView:)]) {
NSInteger listCount = [self.delegate numberOfListsInPagerView:self];
for (NSInteger index = 0; index < listCount; index ++) {
if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:listIdentifierAtIndex:)]) {
NSString *listIdentifier = [self.delegate pagerView:self listIdentifierAtIndex:index];
[newListIdentifierArray addObject:listIdentifier];
}
}
}
NSArray *existedKeys = self.listCache.allKeys;
for (NSString *listIdentifier in existedKeys) {
if (![newListIdentifierArray containsObject:listIdentifier]) {
[self.listCache removeObjectForKey:listIdentifier];
}
}
}
[self refreshTableHeaderView];
if (self.pinSectionHeaderVerticalOffset != 0 && self.mainTableView.contentOffset.y > self.pinSectionHeaderVerticalOffset) {
self.mainTableView.contentOffset = CGPointZero;
}
[self.mainTableView reloadData];
[self.listContainerView reloadData];
}
- (void)resizeTableHeaderViewHeightWithAnimatable:(BOOL)animatable duration:(NSTimeInterval)duration curve:(UIViewAnimationCurve)curve {
if (animatable) {
UIViewAnimationOptions options = UIViewAnimationOptionCurveLinear;
switch (curve) {
case UIViewAnimationCurveEaseIn: options = UIViewAnimationOptionCurveEaseIn; break;
case UIViewAnimationCurveEaseOut: options = UIViewAnimationOptionCurveEaseOut; break;
case UIViewAnimationCurveEaseInOut: options = UIViewAnimationOptionCurveEaseInOut; break;
default: break;
}
[UIView animateWithDuration:duration delay:0 options:options animations:^{
CGRect frame = self.tableHeaderContainerView.bounds;
frame.size.height = [self.delegate tableHeaderViewHeightInPagerView:self];
self.tableHeaderContainerView.frame = frame;
self.mainTableView.tableHeaderView = self.tableHeaderContainerView;
[self.mainTableView setNeedsLayout];
[self.mainTableView layoutIfNeeded];
} completion:^(BOOL finished) { }];
}else {
CGRect frame = self.tableHeaderContainerView.bounds;
frame.size.height = [self.delegate tableHeaderViewHeightInPagerView:self];
self.tableHeaderContainerView.frame = frame;
self.mainTableView.tableHeaderView = self.tableHeaderContainerView;
}
}
#pragma mark - Private
- (void)refreshTableHeaderView {
UIView *tableHeaderView = [self.delegate tableHeaderViewInPagerView:self];
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, [self.delegate tableHeaderViewHeightInPagerView:self])];
[containerView addSubview:tableHeaderView];
tableHeaderView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:tableHeaderView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeTop multiplier:1 constant:0];
NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:tableHeaderView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeLeading multiplier:1 constant:0];
NSLayoutConstraint *bottom = [NSLayoutConstraint constraintWithItem:tableHeaderView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
NSLayoutConstraint *trailing = [NSLayoutConstraint constraintWithItem:tableHeaderView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeTrailing multiplier:1 constant:0];
[containerView addConstraints:@[top, leading, bottom, trailing]];
self.tableHeaderContainerView = containerView;
self.mainTableView.tableHeaderView = containerView;
}
- (void)adjustMainScrollViewToTargetContentInsetIfNeeded:(UIEdgeInsets)insets {
if (UIEdgeInsetsEqualToEdgeInsets(insets, self.mainTableView.contentInset) == NO) {
self.mainTableView.delegate = nil;
self.mainTableView.contentInset = insets;
self.mainTableView.delegate = self;
}
}
- (void)listViewDidScroll:(UIScrollView *)scrollView {
self.currentScrollingListView = scrollView;
[self preferredProcessListViewDidScroll:scrollView];
}
//pinSectionHeaderVerticalOffsetMJRefreshJXPagingViewMJRefreshcontentInsetpinSectionHeaderVerticalOffsetcontentInset.top
//https://github.com/pujiaxin33/JXPagingView/issues/203
- (BOOL)isSetMainScrollViewContentInsetToZeroEnabled:(UIScrollView *)scrollView {
//scrollView.contentInset.top0scrollView.contentInset.toppinSectionHeaderVerticalOffsetpinSectionHeaderVerticalOffsetMJRefreshmj_insetT
BOOL isRefreshing = scrollView.contentInset.top != 0 && scrollView.contentInset.top != self.pinSectionHeaderVerticalOffset;
return !isRefreshing;
}
#pragma mark - UITableViewDataSource, UITableViewDelegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return MAX(self.bounds.size.height - [self.delegate heightForPinSectionHeaderInPagerView:self] - self.pinSectionHeaderVerticalOffset, 0);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.backgroundColor = [UIColor clearColor];
if (self.listContainerView.superview != cell.contentView) {
[cell.contentView addSubview:self.listContainerView];
}
if (!CGRectEqualToRect(self.listContainerView.frame, cell.bounds)) {
self.listContainerView.frame = cell.bounds;
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return [self.delegate heightForPinSectionHeaderInPagerView:self];
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return [self.delegate viewForPinSectionHeaderInPagerView:self];
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return 1;
}
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
UIView *footer = [[UIView alloc] initWithFrame:CGRectZero];
footer.backgroundColor = [UIColor clearColor];
return footer;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (self.pinSectionHeaderVerticalOffset != 0) {
if (!(self.currentScrollingListView != nil && self.currentScrollingListView.contentOffset.y > [self minContentOffsetYInListScrollView:self.currentScrollingListView])) {
//listView
if (scrollView.contentOffset.y >= self.pinSectionHeaderVerticalOffset) {
//contentInset.top
[self adjustMainScrollViewToTargetContentInsetIfNeeded:UIEdgeInsetsMake(self.pinSectionHeaderVerticalOffset, 0, 0, 0)];
}else {
if ([self isSetMainScrollViewContentInsetToZeroEnabled:scrollView]) {
[self adjustMainScrollViewToTargetContentInsetIfNeeded:UIEdgeInsetsZero];
}
}
}
}
[self preferredProcessMainTableViewDidScroll:scrollView];
if (self.delegate && [self.delegate respondsToSelector:@selector(mainTableViewDidScroll:)]) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
[self.delegate mainTableViewDidScroll:scrollView];
#pragma GCC diagnostic pop
}
if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewDidScroll:)]) {
[self.delegate pagerView:self mainTableViewDidScroll:scrollView];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.listContainerView.scrollView.scrollEnabled = NO;
if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewWillBeginDragging:)]) {
[self.delegate pagerView:self mainTableViewWillBeginDragging:scrollView];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (self.isListHorizontalScrollEnabled && !decelerate) {
self.listContainerView.scrollView.scrollEnabled = YES;
}
if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewDidEndDragging:willDecelerate:)]) {
[self.delegate pagerView:self mainTableViewDidEndDragging:scrollView willDecelerate:decelerate];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (self.isListHorizontalScrollEnabled) {
self.listContainerView.scrollView.scrollEnabled = YES;
}
if ([self isSetMainScrollViewContentInsetToZeroEnabled:scrollView]) {
if (self.mainTableView.contentInset.top != 0 && self.pinSectionHeaderVerticalOffset != 0) {
[self adjustMainScrollViewToTargetContentInsetIfNeeded:UIEdgeInsetsZero];
}
}
if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewDidEndDecelerating:)]) {
[self.delegate pagerView:self mainTableViewDidEndDecelerating:scrollView];
}
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
if (self.isListHorizontalScrollEnabled) {
self.listContainerView.scrollView.scrollEnabled = YES;
}
if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewDidEndScrollingAnimation:)]) {
[self.delegate pagerView:self mainTableViewDidEndScrollingAnimation:scrollView];
}
}
#pragma mark - JXPagerListContainerViewDelegate
- (NSInteger)numberOfListsInlistContainerView:(JXPagerListContainerView *)listContainerView {
return [self.delegate numberOfListsInPagerView:self];
}
- (id<JXPagerViewListViewDelegate>)listContainerView:(JXPagerListContainerView *)listContainerView initListForIndex:(NSInteger)index {
id<JXPagerViewListViewDelegate> list = self.validListDict[@(index)];
if (list == nil) {
if (self.allowsCacheList && self.delegate && [self.delegate respondsToSelector:@selector(pagerView:listIdentifierAtIndex:)]) {
NSString *listIdentifier = [self.delegate pagerView:self listIdentifierAtIndex:index];
list = self.listCache[listIdentifier];
}
}
if (list == nil) {
list = [self.delegate pagerView:self initListAtIndex:index];
__weak typeof(self)weakSelf = self;
__weak typeof(id<JXPagerViewListViewDelegate>) weakList = list;
[list listViewDidScrollCallback:^(UIScrollView *scrollView) {
weakSelf.currentList = weakList;
[weakSelf listViewDidScroll:scrollView];
}];
_validListDict[@(index)] = list;
if (self.allowsCacheList && self.delegate && [self.delegate respondsToSelector:@selector(pagerView:listIdentifierAtIndex:)]) {
NSString *listIdentifier = [self.delegate pagerView:self listIdentifierAtIndex:index];
self.listCache[listIdentifier] = list;
}
}
return list;
}
- (void)listContainerViewWillBeginDragging:(JXPagerListContainerView *)listContainerView {
self.mainTableView.scrollEnabled = NO;
}
- (void)listContainerViewWDidEndScroll:(JXPagerListContainerView *)listContainerView {
self.mainTableView.scrollEnabled = YES;
}
- (void)listContainerView:(JXPagerListContainerView *)listContainerView listDidAppearAtIndex:(NSInteger)index {
self.currentScrollingListView = [self.validListDict[@(index)] listScrollView];
for (id<JXPagerViewListViewDelegate> listItem in self.validListDict.allValues) {
if (listItem == self.validListDict[@(index)]) {
[listItem listScrollView].scrollsToTop = YES;
}else {
[listItem listScrollView].scrollsToTop = NO;
}
}
}
- (Class)scrollViewClassInlistContainerView:(JXPagerListContainerView *)listContainerView {
if (self.delegate && [self.delegate respondsToSelector:@selector(scrollViewClassInlistContainerViewInPagerView:)]) {
return [self.delegate scrollViewClassInlistContainerViewInPagerView:self];
}
return nil;
}
@end
@implementation JXPagerView (UISubclassingGet)
- (CGFloat)mainTableViewMaxContentOffsetY {
return [self.delegate tableHeaderViewHeightInPagerView:self] - self.pinSectionHeaderVerticalOffset;
}
@end
@implementation JXPagerView (UISubclassingHooks)
- (void)preferredProcessListViewDidScroll:(UIScrollView *)scrollView {
if (self.mainTableView.contentOffset.y < self.mainTableViewMaxContentOffsetY) {
//mainTableViewheaderlistScrollView0
if (self.currentList && [self.currentList respondsToSelector:@selector(listScrollViewWillResetContentOffset)]) {
[self.currentList listScrollViewWillResetContentOffset];
}
[self setListScrollViewToMinContentOffsetY:scrollView];
if (self.automaticallyDisplayListVerticalScrollIndicator) {
scrollView.showsVerticalScrollIndicator = NO;
}
}else {
//mainTableViewheadermainTableViewlistScrollView
self.mainTableView.contentOffset = CGPointMake(0, self.mainTableViewMaxContentOffsetY);
if (self.automaticallyDisplayListVerticalScrollIndicator) {
scrollView.showsVerticalScrollIndicator = YES;
}
}
}
- (void)preferredProcessMainTableViewDidScroll:(UIScrollView *)scrollView {
if (self.currentScrollingListView != nil && self.currentScrollingListView.contentOffset.y > [self minContentOffsetYInListScrollView:self.currentScrollingListView]) {
//mainTableViewheaderlistViewmainTableViewcontentOffset
[self setMainTableViewToMaxContentOffsetY];
}
if (scrollView.contentOffset.y < self.mainTableViewMaxContentOffsetY) {
//mainTableViewheaderlistViewcontentOffset
for (id<JXPagerViewListViewDelegate> list in self.validListDict.allValues) {
if ([list respondsToSelector:@selector(listScrollViewWillResetContentOffset)]) {
[list listScrollViewWillResetContentOffset];
}
[self setListScrollViewToMinContentOffsetY:[list listScrollView]];
}
}
if (scrollView.contentOffset.y > self.mainTableViewMaxContentOffsetY && self.currentScrollingListView.contentOffset.y == [self minContentOffsetYInListScrollView:self.currentScrollingListView]) {
//mainTableViewheaderViewlistView
[self setMainTableViewToMaxContentOffsetY];
}
}
- (void)setMainTableViewToMaxContentOffsetY {
self.mainTableView.contentOffset = CGPointMake(0, self.mainTableViewMaxContentOffsetY);
}
- (void)setListScrollViewToMinContentOffsetY:(UIScrollView *)scrollView {
scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, [self minContentOffsetYInListScrollView:scrollView]);
}
- (CGFloat)minContentOffsetYInListScrollView:(UIScrollView *)scrollView {
if (@available(iOS 11.0, *)) {
return -scrollView.adjustedContentInset.top;
}
return -scrollView.contentInset.top;
}
@end

Some files were not shown because too many files have changed in this diff Show More