Compare commits
48 Commits
998fa7aa67
...
fac5e7657c
| Author | SHA1 | Date | |
|---|---|---|---|
| fac5e7657c | |||
| 50dcb78417 | |||
| 5e1a1f540e | |||
| 2415e97c97 | |||
| aa71cc3c4f | |||
| e5ddcc4308 | |||
| 883b222254 | |||
| dc9ee10023 | |||
| 2c4a4329ff | |||
| 553238de0c | |||
| 80b6102673 | |||
| 705b0f374e | |||
| 5bdc7ddec0 | |||
| 5d2a3de2f4 | |||
| 675a9f6d64 | |||
| 41b14ceea4 | |||
| a729396401 | |||
| 3b0beb52da | |||
| faeb930fe3 | |||
| 9a39c29e88 | |||
| b23c9a678b | |||
| 96cd32ed99 | |||
| 50dd53b0c0 | |||
| 48a12f0919 | |||
| 91d754b389 | |||
| 450798c8bd | |||
| c3acc11f6a | |||
| d592c9f12e | |||
| 26e39ce416 | |||
| 32521208a0 | |||
| 6e969648c6 | |||
| 074596ebcb | |||
| f0542c11c8 | |||
| 0fa3d10284 | |||
| 0d13192723 | |||
| a72aae84ef | |||
| 6ba1339c0b | |||
| a75afbe4c1 | |||
| 1f45564539 | |||
| 41aec6b89e | |||
| a1db745b6c | |||
| 15fc9621cd | |||
| d7874829d9 | |||
| abf32e8457 | |||
| efdcf60ed1 | |||
| 7a1b17d060 | |||
| f43f94b94d | |||
| 3e2dc4bcb6 |
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>kbkeyboardAppExtension</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#import "Masonry.h"
|
||||
#import "KBAuthManager.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
|
||||
@@ -22,6 +23,7 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示)
|
||||
@property (nonatomic, strong) KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示)
|
||||
@property (nonatomic, strong) KBSettingView *settingView; // 设置页
|
||||
@property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层)
|
||||
@end
|
||||
|
||||
@implementation KeyboardViewController
|
||||
@@ -40,12 +42,32 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
__unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||
// 如需,可在此刷新与完全访问相关的 UI
|
||||
}];
|
||||
|
||||
// 皮肤变化时,立即应用
|
||||
__unused id token2 = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
|
||||
[self kb_applyTheme];
|
||||
}];
|
||||
[self kb_applyTheme];
|
||||
}
|
||||
|
||||
|
||||
- (void)setupUI {
|
||||
// 固定键盘整体高度
|
||||
[self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT].active = YES;
|
||||
// 固定键盘整体高度;为减少与系统自调整高度产生的告警,将优先级设置为 High
|
||||
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT];
|
||||
h.priority = UILayoutPriorityDefaultHigh; // 750
|
||||
h.active = YES;
|
||||
// 关闭 UIInputView 自适应(某些系统版本会尝试放大为全屏高度导致冲突)
|
||||
if ([self.view isKindOfClass:[UIInputView class]]) {
|
||||
UIInputView *iv = (UIInputView *)self.view;
|
||||
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
|
||||
iv.allowsSelfSizing = NO;
|
||||
}
|
||||
}
|
||||
// 背景图铺底
|
||||
[self.view addSubview:self.bgImageView];
|
||||
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self.view);
|
||||
}];
|
||||
// 预置功能面板(默认隐藏),与键盘区域共享相同布局
|
||||
self.functionView.hidden = YES;
|
||||
[self.view addSubview:self.functionView];
|
||||
@@ -209,7 +231,8 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
}
|
||||
|
||||
- (void)kb_tryOpenContainerForLoginIfNeeded {
|
||||
NSURL *url = [NSURL URLWithString:@"kbkeyboard://login?src=keyboard"];
|
||||
// 使用与主 App 一致的自定义 Scheme
|
||||
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
|
||||
if (!url) return;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
|
||||
@@ -217,4 +240,40 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
|
||||
__unused typeof(weakSelf) selfStrong = weakSelf;
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
|
||||
self.bgImageView.image = img;
|
||||
BOOL hasImg = (img != nil);
|
||||
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
|
||||
// 触发键区按主题重绘
|
||||
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||
// method declared in KBKeyBoardMainView.h
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[self.functionView performSelector:@selector(kb_applyTheme)];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIImageView *)bgImageView {
|
||||
if (!_bgImageView) {
|
||||
_bgImageView = [[UIImageView alloc] init];
|
||||
_bgImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_bgImageView.clipsToBounds = YES;
|
||||
}
|
||||
return _bgImageView;
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -23,9 +23,15 @@
|
||||
|
||||
// 通用链接(Universal Links)统一配置
|
||||
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
|
||||
#define KB_UL_BASE @"https://your.domain/ul" // 替换为你的真实域名与前缀路径
|
||||
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
|
||||
#define KB_UL_LOGIN KB_UL_BASE @"/login"
|
||||
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
|
||||
|
||||
// 在扩展内,启用 URL Bridge(仅在显式的用户点击动作中使用)
|
||||
// 这样即便宿主 App(如备忘录)拒绝 extensionContext 的 openURL,仍可通过响应链兜底拉起容器 App。
|
||||
#ifndef KB_URL_BRIDGE_ENABLE
|
||||
#define KB_URL_BRIDGE_ENABLE 1
|
||||
#endif
|
||||
|
||||
|
||||
#endif /* PrefixHeader_pch */
|
||||
|
||||
30
CustomKeyboard/Utils/KBURLOpenBridge.h
Normal file
30
CustomKeyboard/Utils/KBURLOpenBridge.h
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// KBURLOpenBridge.h
|
||||
// 非公开:通过响应链查找 `openURL:` 选择器,尝试在扩展环境中打开自定义 scheme。
|
||||
// 警告:存在审核风险。默认仅 Debug 启用(见 KB_URL_BRIDGE_ENABLE)。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#ifndef KB_URL_BRIDGE_ENABLE
|
||||
#if DEBUG
|
||||
#define KB_URL_BRIDGE_ENABLE 1
|
||||
#else
|
||||
#define KB_URL_BRIDGE_ENABLE 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@interface KBURLOpenBridge : NSObject
|
||||
|
||||
/// 尝试通过响应链调用 openURL:(仅在 KB_URL_BRIDGE_ENABLE 为 1 时执行)。
|
||||
/// @param url 自定义 scheme,如 kbkeyboard://settings
|
||||
/// @param start 起始 responder(传 self 或任意视图)
|
||||
/// @return 是否看起来已发起打开动作(不保证一定成功)
|
||||
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
46
CustomKeyboard/Utils/KBURLOpenBridge.m
Normal file
46
CustomKeyboard/Utils/KBURLOpenBridge.m
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// KBURLOpenBridge.m
|
||||
//
|
||||
|
||||
#import "KBURLOpenBridge.h"
|
||||
#import <objc/message.h>
|
||||
|
||||
@implementation KBURLOpenBridge
|
||||
|
||||
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
|
||||
#if KB_URL_BRIDGE_ENABLE
|
||||
if (!url || !start) return NO;
|
||||
SEL sel = NSSelectorFromString(@"openURL:");
|
||||
UIResponder *responder = start;
|
||||
while (responder) {
|
||||
@try {
|
||||
if ([responder respondsToSelector:sel]) {
|
||||
// 尽量按签名调用;若失败则回退 performSelector
|
||||
BOOL handled = NO;
|
||||
// 尝试 (BOOL)openURL:(NSURL *)
|
||||
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
|
||||
if (funcBool) {
|
||||
handled = funcBool(responder, sel, url);
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[responder performSelector:sel withObject:url];
|
||||
handled = YES;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
} @catch (__unused NSException *e) {
|
||||
// ignore and continue
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return NO;
|
||||
#else
|
||||
(void)url; (void)start;
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
#import "KBHUD.h"
|
||||
#import "KBURLOpenBridge.h"
|
||||
|
||||
@interface KBFullAccessGuideView ()
|
||||
@property (nonatomic, strong) UIControl *backdrop;
|
||||
@property (nonatomic, strong) UIView *card;
|
||||
// 预先保存当前键盘控制器,避免运行时通过响应链找不到
|
||||
@property (nonatomic, weak) UIInputViewController *ivc;
|
||||
@end
|
||||
|
||||
@implementation KBFullAccessGuideView
|
||||
@@ -109,7 +114,8 @@
|
||||
}
|
||||
|
||||
- (void)presentIn:(UIView *)parent {
|
||||
UIView *container = parent.window ?: parent;
|
||||
if (!parent) return;
|
||||
UIView *container = parent; // 关键:加到键盘视图树中,而不是 window
|
||||
self.frame = container.bounds;
|
||||
self.alpha = 0;
|
||||
[container addSubview:self];
|
||||
@@ -125,15 +131,19 @@
|
||||
|
||||
+ (void)showInView:(UIView *)parent {
|
||||
if (!parent) return;
|
||||
// 避免重复
|
||||
for (UIView *v in (parent.window ?: parent).subviews) {
|
||||
// 避免重复(仅在 parent 层级检查)
|
||||
for (UIView *v in parent.subviews) {
|
||||
if ([v isKindOfClass:[KBFullAccessGuideView class]]) return;
|
||||
}
|
||||
[[KBFullAccessGuideView build] presentIn:parent];
|
||||
KBFullAccessGuideView *view = [KBFullAccessGuideView build];
|
||||
// 预取 ivc
|
||||
view.ivc = KBFindInputViewController(parent);
|
||||
[view presentIn:parent];
|
||||
}
|
||||
|
||||
+ (void)dismissFromView:(UIView *)parent {
|
||||
UIView *container = parent.window ?: parent;
|
||||
UIView *container = parent;
|
||||
if (!container) return;
|
||||
for (UIView *v in container.subviews) {
|
||||
if ([v isKindOfClass:[KBFullAccessGuideView class]]) {
|
||||
[(KBFullAccessGuideView *)v dismiss];
|
||||
@@ -146,40 +156,74 @@
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (UIInputViewController *)kb_findInputController {
|
||||
UIResponder *res = self;
|
||||
while (res) {
|
||||
if ([res isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)res;
|
||||
}
|
||||
res = res.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
// 打开主 App,引导用户去系统设置开启完全访问:优先 Scheme,失败再试 UL;仍失败则提示手动路径。
|
||||
- (void)onTapGoEnable {
|
||||
// 在扩展中无法使用 UIApplication。改为委托宿主打开链接:
|
||||
// 方案:优先拉起主 App 并由主 App 打开设置页,避免宿主拦截。
|
||||
UIInputViewController *ivc = [self kb_findInputController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { [self dismiss]; return; }
|
||||
|
||||
// 先尝试 Universal Link(如未配置可改为你的域名),失败再用自定义 scheme。
|
||||
|
||||
// 自定义 Scheme(App 里在 openURL 中转到设置页)
|
||||
// 统一使用主 App 的自定义 Scheme
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//settings?src=kb_extension", KB_APP_SCHEME]];
|
||||
// Universal Link(需 AASA/Associated Domains 配置且 KB_UL_BASE 与域名一致)
|
||||
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
|
||||
void (^fallback)(void) = ^{
|
||||
NSURL *scheme = [NSURL URLWithString:@"kbkeyboard://settings?src=kb_extension"]; // 主App在 openURL 中处理
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(__unused BOOL ok2) {
|
||||
// 无论成功与否,都收起当前提示层,避免遮挡
|
||||
[self dismiss];
|
||||
}];
|
||||
|
||||
void (^finish)(BOOL) = ^(BOOL ok){
|
||||
if (ok) { [self dismiss]; }
|
||||
else {
|
||||
[KBHUD showInfo:@"无法自动打开,请按路径:设置→通用→键盘→键盘→恋爱键盘→允许完全访问"]; // 失败兜底提示
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 先试 Scheme(更可能被宿主允许直接拉起 App)
|
||||
if (scheme) {
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok) {
|
||||
if (ok) { finish(YES); return; }
|
||||
if (ul) {
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok2) {
|
||||
if (ok2) { finish(YES); return; }
|
||||
// 兜底:在用户点击触发的场景下,尝试通过响应链调用 openURL:
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
if (!bridged && ul) {
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}];
|
||||
} else {
|
||||
// 没有 UL,则直接尝试桥接 Scheme
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
// 无 scheme 时,直接尝试 UL
|
||||
if (ul) {
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
|
||||
if (ok) { [self dismiss]; }
|
||||
else { fallback(); }
|
||||
if (ok) { finish(YES); return; }
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
finish(bridged);
|
||||
}];
|
||||
} else {
|
||||
fallback();
|
||||
finish(NO);
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBResponderUtils.h" // 查找 UIInputViewController,用于系统切换输入法
|
||||
|
||||
@interface KBFunctionBarView ()
|
||||
@property (nonatomic, strong) UIView *leftContainer; // 左侧按钮容器
|
||||
@property (nonatomic, strong) UIView *rightContainer; // 右侧按钮容器
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *rightButtonsInternal;
|
||||
@property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键
|
||||
@end
|
||||
|
||||
@implementation KBFunctionBarView
|
||||
@@ -38,6 +40,7 @@
|
||||
- (void)buildUI {
|
||||
// 左右两个容器,方便分别布局
|
||||
[self addSubview:self.leftContainer];
|
||||
[self addSubview:self.globeButtonInternal];
|
||||
[self addSubview:self.rightContainer];
|
||||
|
||||
[self.rightContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
@@ -46,8 +49,15 @@
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
// 左侧地球键(按需显示)
|
||||
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
make.right.equalTo(self.rightContainer.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(36);
|
||||
@@ -113,6 +123,9 @@
|
||||
}
|
||||
|
||||
self.rightButtonsInternal = rightBtns.copy;
|
||||
|
||||
// 初始刷新地球键可见性与事件绑定
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
@@ -158,4 +171,56 @@
|
||||
return _rightContainer;
|
||||
}
|
||||
|
||||
- (UIButton *)globeButtonInternal {
|
||||
if (!_globeButtonInternal) {
|
||||
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_globeButtonInternal.layer.cornerRadius = 16;
|
||||
_globeButtonInternal.layer.masksToBounds = YES;
|
||||
_globeButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
[_globeButtonInternal setTitle:@"🌐" forState:UIControlStateNormal];
|
||||
[_globeButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
}
|
||||
return _globeButtonInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Globe (Input Mode Switch)
|
||||
|
||||
- (void)kb_refreshGlobeVisibility {
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
BOOL needSwitchKey = YES;
|
||||
if (ivc && [ivc respondsToSelector:@selector(needsInputModeSwitchKey)]) {
|
||||
needSwitchKey = ivc.needsInputModeSwitchKey;
|
||||
}
|
||||
|
||||
self.globeButtonInternal.hidden = !needSwitchKey;
|
||||
|
||||
// 左容器左约束:根据是否显示地球键动态调整
|
||||
[self.leftContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
if (needSwitchKey) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
} else {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
}
|
||||
make.right.equalTo(self.rightContainer.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
// 绑定系统输入法切换事件
|
||||
[self.globeButtonInternal removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
|
||||
if (needSwitchKey && ivc) {
|
||||
SEL sel = NSSelectorFromString(@"handleInputModeListFromView:withEvent:");
|
||||
if ([ivc respondsToSelector:sel]) {
|
||||
[self.globeButtonInternal addTarget:ivc action:sel forControlEvents:UIControlEventAllTouchEvents];
|
||||
} else {
|
||||
[self.globeButtonInternal addTarget:ivc action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -33,6 +33,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (nonatomic, strong, readonly) UIButton *clearButton; // 右侧-清空
|
||||
@property (nonatomic, strong, readonly) UIButton *sendButton; // 右侧-发送
|
||||
|
||||
/// 应用当前皮肤(更新背景/强调色)
|
||||
- (void)kb_applyTheme;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "KBFunctionPasteView.h"
|
||||
#import "KBFunctionTagCell.h"
|
||||
@@ -13,6 +14,8 @@
|
||||
#import <MBProgressHUD.h>
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "KBFullAccessManager.h"
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBURLOpenBridge.h" // 兜底从扩展侧直接尝试 openURL:
|
||||
|
||||
static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
@@ -39,20 +42,33 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
// 整体绿色背景(接近截图效果,项目可自行替换素材)
|
||||
self.backgroundColor = [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
||||
// 背景使用当前主题强调色
|
||||
[self kb_applyTheme];
|
||||
|
||||
[self setupUI];
|
||||
[self reloadDemoData];
|
||||
|
||||
// 初始化剪贴板监控状态
|
||||
_lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount;
|
||||
|
||||
// 监听“完全访问”状态变化,动态启停剪贴板监控,避免在未开完全访问时触发 TCC/XPC 错误日志
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
KBSkinManager *mgr = [KBSkinManager shared];
|
||||
UIColor *accent = mgr.current.accentColor ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
|
||||
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
|
||||
self.backgroundColor = hasImg ? [accent colorWithAlphaComponent:0.65] : accent;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopPasteboardMonitor];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
@@ -81,10 +97,11 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[self.rightButtonContainer addSubview:self.clearButtonInternal];
|
||||
[self.rightButtonContainer addSubview:self.sendButtonInternal];
|
||||
|
||||
// 竖向排布:粘贴、删除、清空为等高,发送略高
|
||||
// 竖向排布:粘贴、删除、清空为等高;发送优先更高,但允许在空间不足时压缩
|
||||
CGFloat smallH = 44;
|
||||
CGFloat bigH = 56;
|
||||
CGFloat vSpace = 10;
|
||||
// 原 10 在键盘总高度 276 下容易超出容器,改为 8 以避免 AutoLayout 冲突
|
||||
CGFloat vSpace = 8;
|
||||
[self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.rightButtonContainer.mas_top);
|
||||
make.left.right.equalTo(self.rightButtonContainer);
|
||||
@@ -103,8 +120,10 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace);
|
||||
make.left.right.equalTo(self.rightButtonContainer);
|
||||
make.height.mas_equalTo(bigH);
|
||||
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom); // 底部可伸缩
|
||||
// 允许在空间不足时缩短到 smallH,避免产生约束冲突
|
||||
make.height.greaterThanOrEqualTo(@(smallH));
|
||||
make.height.lessThanOrEqualTo(@(bigH));
|
||||
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom);
|
||||
}];
|
||||
|
||||
// 2. 粘贴区(位于右侧按钮左侧)
|
||||
@@ -169,7 +188,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[KBHUD showInfo:@"处理中…"];
|
||||
// return;
|
||||
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) return;
|
||||
|
||||
NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @"";
|
||||
@@ -182,9 +201,22 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
|
||||
if (ok) return; // Universal Link 成功
|
||||
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"kbkeyboard://login?src=functionView&index=%ld&title=%@", (long)indexPath.item, encodedTitle]];
|
||||
// 统一使用主 App 注册的自定义 Scheme
|
||||
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
|
||||
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
|
||||
if (!ok2) {
|
||||
if (ok2) return;
|
||||
|
||||
// 兜底:在用户点击触发的场景下,尝试通过响应链调用 openURL:
|
||||
// 以提升在“备忘录”等宿主中的成功率。
|
||||
BOOL bridged = NO;
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (__unused NSException *e) { bridged = NO; }
|
||||
|
||||
if (!bridged) {
|
||||
// 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager 引导。
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
|
||||
}
|
||||
@@ -224,6 +256,8 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
// - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。
|
||||
|
||||
- (void)startPasteboardMonitor {
|
||||
// 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志
|
||||
if (![[KBFullAccessManager shared] hasFullAccess]) return;
|
||||
if (self.pasteboardTimer) return;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
@@ -248,34 +282,40 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
if (self.window && !self.isHidden) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
[self kb_refreshPasteboardMonitor];
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden {
|
||||
BOOL wasHidden = self.isHidden;
|
||||
[super setHidden:hidden];
|
||||
if (wasHidden != hidden) {
|
||||
if (!hidden && self.window) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
[self kb_refreshPasteboardMonitor];
|
||||
}
|
||||
}
|
||||
|
||||
// 根据窗口可见性与完全访问状态,统一启停粘贴板监控
|
||||
- (void)kb_refreshPasteboardMonitor {
|
||||
BOOL visible = (self.window && !self.isHidden);
|
||||
if (visible && [[KBFullAccessManager shared] hasFullAccess]) {
|
||||
[self startPasteboardMonitor];
|
||||
} else {
|
||||
[self stopPasteboardMonitor];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)kb_fullAccessChanged {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
|
||||
}
|
||||
- (void)onTapDelete {
|
||||
NSLog(@"点击:删除");
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
- (void)onTapClear {
|
||||
NSLog(@"点击:清空");
|
||||
// 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容)
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
// 逐批读取 documentContextBeforeInput 并删除,避免 50 字符窗口限制带来的残留
|
||||
NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞
|
||||
@@ -292,7 +332,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
- (void)onTapSend {
|
||||
NSLog(@"点击:发送");
|
||||
// 发送:插入换行。大多数聊天类 App 会把回车视为“发送”
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy insertText:@"\n"];
|
||||
}
|
||||
@@ -417,16 +457,6 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
#pragma mark - Find Owner Controller
|
||||
|
||||
// 在视图的响应链中查找宿主 UIInputViewController(KeyboardViewController)
|
||||
- (UIInputViewController *)findInputViewController {
|
||||
UIResponder *responder = self;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
|
||||
@end
|
||||
|
||||
@@ -28,6 +28,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface KBKeyBoardMainView : UIView
|
||||
@property (nonatomic, weak) id<KBKeyBoardMainViewDelegate> delegate;
|
||||
|
||||
/// 应用当前皮肤(会触发键区重载以应用按键颜色)
|
||||
- (void)kb_applyTheme;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBKey.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate>
|
||||
@property (nonatomic, strong) KBToolBar *topBar;
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
|
||||
// 顶部栏
|
||||
self.topBar = [[KBToolBar alloc] init];
|
||||
self.topBar.delegate = self;
|
||||
@@ -31,7 +32,7 @@
|
||||
make.top.equalTo(self.mas_top).offset(6);
|
||||
make.height.mas_equalTo(40);
|
||||
}];
|
||||
|
||||
|
||||
// 键盘区域
|
||||
self.keyboardView = [[KBKeyboardView alloc] init];
|
||||
self.keyboardView.delegate = self;
|
||||
@@ -41,9 +42,9 @@
|
||||
make.top.equalTo(self.topBar.mas_bottom).offset(4);
|
||||
make.bottom.equalTo(self.mas_bottom).offset(-4);
|
||||
}];
|
||||
|
||||
|
||||
// 功能面板切换交由外部控制器处理;此处不直接创建/管理
|
||||
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -114,5 +115,15 @@
|
||||
// 切换功能面板交由外部控制器处理(此处不再实现)
|
||||
|
||||
// 设置页展示改由 KeyboardViewController 统一处理
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)kb_applyTheme {
|
||||
KBSkinManager *mgr = [KBSkinManager shared];
|
||||
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
|
||||
UIColor *bg = mgr.current.keyboardBackground;
|
||||
self.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
||||
self.keyboardView.backgroundColor = hasImg ? [UIColor clearColor] : bg;
|
||||
[self.keyboardView reloadKeys];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.h"
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@implementation KBKeyButton
|
||||
|
||||
@@ -16,10 +17,11 @@
|
||||
}
|
||||
|
||||
- (void)applyDefaultStyle {
|
||||
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; // 字体样式
|
||||
[self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
[self setTitleColor:[UIColor blackColor] forState:UIControlStateHighlighted];
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
[self setTitleColor:t.keyTextColor forState:UIControlStateNormal];
|
||||
[self setTitleColor:t.keyTextColor forState:UIControlStateHighlighted];
|
||||
self.backgroundColor = t.keyBackground;
|
||||
self.layer.cornerRadius = 6.0; // 圆角
|
||||
self.layer.masksToBounds = NO;
|
||||
self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; // 阴影效果
|
||||
@@ -46,10 +48,11 @@
|
||||
|
||||
- (void)refreshStateAppearance {
|
||||
// 选中态用于 Shift/CapsLock 等特殊按键的高亮显示
|
||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||
if (self.isSelected) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0];
|
||||
self.backgroundColor = t.keyHighlightBackground ?: t.keyBackground;
|
||||
} else {
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.backgroundColor = t.keyBackground;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#import "KBKeyboardView.h"
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.h"
|
||||
#import "KBResponderUtils.h" // 封装的响应链工具
|
||||
#import "KBSkinManager.h"
|
||||
|
||||
@interface KBKeyboardView ()
|
||||
@property (nonatomic, strong) UIView *row1;
|
||||
@@ -13,13 +15,15 @@
|
||||
@property (nonatomic, strong) UIView *row3;
|
||||
@property (nonatomic, strong) UIView *row4;
|
||||
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
|
||||
// 长按退格的一次次删除控制标记(不使用 NSTimer,仅用 GCD 递归调度)
|
||||
@property (nonatomic, assign) BOOL backspaceHoldActive;
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
self.backgroundColor = [KBSkinManager shared].current.keyboardBackground;
|
||||
_layoutStyle = KBKeyboardLayoutStyleLetters;
|
||||
// 默认小写:与需求一致,初始不开启 Shift
|
||||
_shiftOn = NO;
|
||||
@@ -196,6 +200,16 @@
|
||||
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[row addSubview:btn];
|
||||
|
||||
// ⌫ 长按:开始连续逐个删除(无需 NSTimer)。使用 UILongPressGestureRecognizer 识别长按,
|
||||
// 在开始态触发递归的轻量调度,每次删除 1 个字符,直到松手或无内容。
|
||||
if (key.type == KBKeyTypeBackspace) {
|
||||
UILongPressGestureRecognizer *lp = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onBackspaceLongPress:)];
|
||||
// 稍短的判定时间,提升响应(默认约 0.5s)。
|
||||
lp.minimumPressDuration = 0.35;
|
||||
lp.cancelsTouchesInView = YES; // 被识别为长按时,取消普通点击
|
||||
[btn addGestureRecognizer:lp];
|
||||
}
|
||||
|
||||
// Shift 按钮选中态随大小写状态变化
|
||||
if (key.type == KBKeyTypeShift) {
|
||||
btn.selected = self.shiftOn;
|
||||
@@ -250,6 +264,9 @@
|
||||
if (firstChar) {
|
||||
for (KBKeyButton *b in row.subviews) {
|
||||
if (![b isKindOfClass:[KBKeyButton class]]) continue;
|
||||
// 当本行没有字符键时,firstChar 可能是一个“特殊键”,
|
||||
// 避免对基准按钮自身添加 self == self * k 的无效约束
|
||||
if (b == firstChar) continue;
|
||||
if (b.key.type == KBKeyTypeCharacter) continue;
|
||||
CGFloat multiplier = 1.5;
|
||||
if (b.key.type == KBKeyTypeSpace) multiplier = 4.0;
|
||||
@@ -293,6 +310,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 长按退格:按住时以小间隔逐个删除;松手停止。(不使用 NSTimer/DisplayLink)
|
||||
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
||||
switch (gr.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
self.backspaceHoldActive = YES;
|
||||
[self kb_backspaceStep];
|
||||
} break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
self.backspaceHoldActive = NO;
|
||||
} break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
// 单步删除并在需要时安排下一次,直到松手或无内容
|
||||
- (void)kb_backspaceStep {
|
||||
if (!self.backspaceHoldActive) { return; }
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
if (before.length <= 0) { self.backspaceHoldActive = NO; return; }
|
||||
[proxy deleteBackward]; // 每次仅删 1 个
|
||||
|
||||
// 轻量递归调度下一次;不使用 NSTimer,避免复杂状态管理
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.06 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||
[selfStrong kb_backspaceStep];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; }
|
||||
|
||||
28
CustomKeyboard/View/KBResponderUtils.h
Normal file
28
CustomKeyboard/View/KBResponderUtils.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// KBResponderUtils.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 统一封装:从任意 UIView/UIResponder 起,向响应链上查找 UIInputViewController。
|
||||
// 作为 header‑only 的工具,便于多处直接引入使用。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#ifndef KBResponderUtils_h
|
||||
#define KBResponderUtils_h
|
||||
|
||||
/// 从给定 responder 开始,沿响应链查找宿主 UIInputViewController。
|
||||
/// 用法:UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
static inline UIInputViewController *KBFindInputViewController(UIResponder *start) {
|
||||
UIResponder *responder = start;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#endif /* KBResponderUtils_h */
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
//
|
||||
|
||||
#import "KBToolBar.h"
|
||||
#import "KBResponderUtils.h" // 查找 UIInputViewController,用于系统切换输入法
|
||||
|
||||
@interface KBToolBar ()
|
||||
@property (nonatomic, strong) UIView *leftContainer;
|
||||
@property (nonatomic, strong) NSArray<UIButton *> *leftButtonsInternal;
|
||||
@property (nonatomic, strong) UIButton *settingsButtonInternal;
|
||||
@property (nonatomic, strong) UIButton *globeButtonInternal; // 可选:系统“切换输入法”键
|
||||
@end
|
||||
|
||||
@implementation KBToolBar
|
||||
@@ -50,6 +52,7 @@
|
||||
- (void)setupUI {
|
||||
[self addSubview:self.leftContainer];
|
||||
[self addSubview:self.settingsButtonInternal];
|
||||
[self addSubview:self.globeButtonInternal];
|
||||
|
||||
// 右侧设置按钮
|
||||
[self.settingsButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
@@ -58,9 +61,16 @@
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 左侧地球键(按需显示)
|
||||
[self.globeButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.width.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 左侧容器占用剩余空间
|
||||
[self.leftContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
make.right.equalTo(self.settingsButtonInternal.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(32);
|
||||
@@ -89,6 +99,9 @@
|
||||
make.right.equalTo(self.leftContainer.mas_right);
|
||||
}];
|
||||
self.leftButtonsInternal = buttons.copy;
|
||||
|
||||
// 初始刷新地球键的可见性与事件绑定
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
- (UIButton *)buildActionButtonAtIndex:(NSInteger)idx {
|
||||
@@ -142,4 +155,59 @@
|
||||
return _settingsButtonInternal;
|
||||
}
|
||||
|
||||
- (UIButton *)globeButtonInternal {
|
||||
if (!_globeButtonInternal) {
|
||||
_globeButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_globeButtonInternal.layer.cornerRadius = 16;
|
||||
_globeButtonInternal.layer.masksToBounds = YES;
|
||||
_globeButtonInternal.backgroundColor = [UIColor colorWithWhite:1 alpha:0.9];
|
||||
[_globeButtonInternal setTitle:@"🌐" forState:UIControlStateNormal];
|
||||
[_globeButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
}
|
||||
return _globeButtonInternal;
|
||||
}
|
||||
|
||||
#pragma mark - Globe (Input Mode Switch)
|
||||
|
||||
// 根据宿主是否已提供系统切换键,决定是否显示地球按钮;并绑定系统事件。
|
||||
- (void)kb_refreshGlobeVisibility {
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
BOOL needSwitchKey = YES;
|
||||
if (ivc && [ivc respondsToSelector:@selector(needsInputModeSwitchKey)]) {
|
||||
needSwitchKey = ivc.needsInputModeSwitchKey; // YES 表示自定义键盘需要提供切换键
|
||||
}
|
||||
|
||||
self.globeButtonInternal.hidden = !needSwitchKey;
|
||||
|
||||
// 重新调整 leftContainer 的左约束:若不需要地球键,则贴左边距 12
|
||||
[self.leftContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
if (needSwitchKey) {
|
||||
make.left.equalTo(self.globeButtonInternal.mas_right).offset(8);
|
||||
} else {
|
||||
make.left.equalTo(self.mas_left).offset(12);
|
||||
}
|
||||
make.right.equalTo(self.settingsButtonInternal.mas_left).offset(-12);
|
||||
make.centerY.equalTo(self.mas_centerY);
|
||||
make.height.mas_equalTo(32);
|
||||
}];
|
||||
|
||||
// 绑定系统提供的输入法切换处理(点按切换、长按弹出列表)
|
||||
// 仅在需要时绑定,避免多余的事件转发
|
||||
[self.globeButtonInternal removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents];
|
||||
if (needSwitchKey && ivc) {
|
||||
SEL sel = NSSelectorFromString(@"handleInputModeListFromView:withEvent:");
|
||||
if ([ivc respondsToSelector:sel]) {
|
||||
[self.globeButtonInternal addTarget:ivc action:sel forControlEvents:UIControlEventAllTouchEvents];
|
||||
} else {
|
||||
// 回退:至少在点按时切换
|
||||
[self.globeButtonInternal addTarget:ivc action:@selector(advanceToNextInputMode) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
[super didMoveToWindow];
|
||||
[self kb_refreshGlobeVisibility];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
10
Podfile
10
Podfile
@@ -8,13 +8,19 @@ target 'keyBoard' do
|
||||
|
||||
pod 'AFNetworking','4.0.1'
|
||||
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']
|
||||
pod 'LYEmptyView', '~> 1.3.1'
|
||||
pod 'Masonry', '1.1.0'
|
||||
pod 'MBProgressHUD', '1.2.0'
|
||||
pod 'MJExtension', '3.4.2'
|
||||
pod 'MJRefresh', '3.7.9'
|
||||
pod 'SDWebImage', '5.21.1'
|
||||
pod 'DZNEmptyDataSet', '1.8.1'
|
||||
pod 'LookinServer', :configurations => ['Debug']
|
||||
end
|
||||
|
||||
target 'CustomKeyboard' do
|
||||
|
||||
33
Podfile.lock
33
Podfile.lock
@@ -16,9 +16,20 @@ PODS:
|
||||
- AFNetworking/NSURLSession
|
||||
- Bugly (2.6.1)
|
||||
- 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)
|
||||
- LSTPopView (0.3.10):
|
||||
- LSTPopView/Code (= 0.3.10)
|
||||
- LSTTimer
|
||||
- LSTPopView/Code (0.3.10):
|
||||
- LSTTimer
|
||||
- LSTTimer (0.2.10)
|
||||
- LYEmptyView (1.3.1)
|
||||
- Masonry (1.1.0)
|
||||
- MBProgressHUD (1.2.0)
|
||||
- MJExtension (3.4.2)
|
||||
@@ -31,7 +42,13 @@ DEPENDENCIES:
|
||||
- AFNetworking (= 4.0.1)
|
||||
- Bugly
|
||||
- DZNEmptyDataSet (= 1.8.1)
|
||||
- FLAnimatedImage (~> 1.0.17)
|
||||
- HWPanModal (~> 0.9.9)
|
||||
- JXCategoryView (~> 1.6.8)
|
||||
- JXPagingView/Pager (~> 2.1.3)
|
||||
- LookinServer
|
||||
- LSTPopView (~> 0.3.10)
|
||||
- LYEmptyView (~> 1.3.1)
|
||||
- Masonry (= 1.1.0)
|
||||
- MBProgressHUD (= 1.2.0)
|
||||
- MJExtension (= 3.4.2)
|
||||
@@ -43,7 +60,14 @@ SPEC REPOS:
|
||||
- AFNetworking
|
||||
- Bugly
|
||||
- DZNEmptyDataSet
|
||||
- FLAnimatedImage
|
||||
- HWPanModal
|
||||
- JXCategoryView
|
||||
- JXPagingView
|
||||
- LookinServer
|
||||
- LSTPopView
|
||||
- LSTTimer
|
||||
- LYEmptyView
|
||||
- Masonry
|
||||
- MBProgressHUD
|
||||
- MJExtension
|
||||
@@ -54,13 +78,20 @@ SPEC CHECKSUMS:
|
||||
AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
|
||||
Bugly: 217ac2ce5f0f2626d43dbaa4f70764c953a26a31
|
||||
DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7
|
||||
FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b
|
||||
HWPanModal: b57a6717d3cdcd666bff44f9dd2a5be9f4d6f5d2
|
||||
JXCategoryView: 262d503acea0b1278c79a1c25b7332ffaef4d518
|
||||
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
|
||||
LookinServer: 1b2b61c6402ae29fa22182d48f5cd067b4e99e80
|
||||
LSTPopView: 9379f00f6ce7d1fc620b50ab00ed3ef97b2d4d52
|
||||
LSTTimer: caf8f02ff366ca175cf4c1778d26c166183c1b6f
|
||||
LYEmptyView: b6d418cfa38b78df0cf243f9a9c25ccbdc399922
|
||||
Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
|
||||
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
|
||||
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
||||
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
|
||||
PODFILE CHECKSUM: e80851eaead44de926040a227bf16809774cc3d2
|
||||
PODFILE CHECKSUM: 74f734479eb0a17a905a260cec140c37703d4479
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
820
Pods/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.m
generated
Executable file
820
Pods/FLAnimatedImage/FLAnimatedImage/FLAnimatedImage.m
generated
Executable file
@@ -0,0 +1,820 @@
|
||||
//
|
||||
// FLAnimatedImage.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "FLAnimatedImage.h"
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#if __has_include(<MobileCoreServices/MobileCoreServices.h>)
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#else
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#endif
|
||||
|
||||
|
||||
// From vm_param.h, define for iOS 8.0 or higher to build on device.
|
||||
#ifndef BYTE_SIZE
|
||||
#define BYTE_SIZE 8 // byte size in bits
|
||||
#endif
|
||||
|
||||
#define MEGABYTE (1024 * 1024)
|
||||
|
||||
// This is how the fastest browsers do it as per 2012: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility
|
||||
const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum = 0.02;
|
||||
|
||||
// An animated image's data size (dimensions * frameCount) category; its value is the max allowed memory (in MB).
|
||||
// E.g.: A 100x200px GIF with 30 frames is ~2.3MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll` category.
|
||||
typedef NS_ENUM(NSUInteger, FLAnimatedImageDataSizeCategory) {
|
||||
FLAnimatedImageDataSizeCategoryAll = 10, // All frames permanently in memory (be nice to the CPU)
|
||||
FLAnimatedImageDataSizeCategoryDefault = 75, // A frame cache of default size in memory (usually real-time performance and keeping low memory profile)
|
||||
FLAnimatedImageDataSizeCategoryOnDemand = 250, // Only keep one frame at the time in memory (easier on memory, slowest performance)
|
||||
FLAnimatedImageDataSizeCategoryUnsupported // Even for one frame too large, computer says no.
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) {
|
||||
FLAnimatedImageFrameCacheSizeNoLimit = 0, // 0 means no specific limit
|
||||
FLAnimatedImageFrameCacheSizeLowMemory = 1, // The minimum frame cache size; this will produce frames on-demand.
|
||||
FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2, // If we can produce the frames faster than we consume, one frame ahead will already result in a stutter-free playback.
|
||||
FLAnimatedImageFrameCacheSizeDefault = 5 // Build up a comfy buffer window to cope with CPU hiccups etc.
|
||||
};
|
||||
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@protocol FLAnimatedImageDebugDelegate <NSObject>
|
||||
@optional
|
||||
- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache;
|
||||
- (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index;
|
||||
- (CGFloat)debug_animatedImagePredrawingSlowdownFactor:(FLAnimatedImage *)animatedImage;
|
||||
@end
|
||||
#endif
|
||||
|
||||
|
||||
@interface FLAnimatedImage ()
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; // The optimal number of frames to cache based on image size & number of frames; never changes
|
||||
@property (nonatomic, assign, readonly, getter=isPredrawingEnabled) BOOL predrawingEnabled; // Enables predrawing of images to improve performance.
|
||||
@property (nonatomic, assign) NSUInteger frameCacheSizeMaxInternal; // Allow to cap the cache size e.g. when memory warnings occur; 0 means no specific limit (default)
|
||||
@property (nonatomic, assign) NSUInteger requestedFrameIndex; // Most recently requested frame index
|
||||
@property (nonatomic, assign, readonly) NSUInteger posterImageFrameIndex; // Index of non-purgable poster image; never changes
|
||||
@property (nonatomic, strong, readonly) NSMutableDictionary *cachedFramesForIndexes;
|
||||
@property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // Indexes of cached frames
|
||||
@property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // Indexes of frames that are currently produced in the background
|
||||
@property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet; // Default index set with the full range of indexes; never changes
|
||||
@property (nonatomic, assign) NSUInteger memoryWarningCount;
|
||||
@property (nonatomic, strong, readonly) dispatch_queue_t serialQueue;
|
||||
@property (nonatomic, strong, readonly) __attribute__((NSObject)) CGImageSourceRef imageSource;
|
||||
|
||||
// The weak proxy is used to break retain cycles with delayed actions from memory warnings.
|
||||
// We are lying about the actual type here to gain static type checking and eliminate casts.
|
||||
// The actual type of the object is `FLWeakProxy`.
|
||||
@property (nonatomic, strong, readonly) FLAnimatedImage *weakProxy;
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@property (nonatomic, weak) id<FLAnimatedImageDebugDelegate> debug_delegate;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// For custom dispatching of memory warnings to avoid deallocation races since NSNotificationCenter doesn't retain objects it is notifying.
|
||||
static NSHashTable *allAnimatedImagesWeak;
|
||||
|
||||
@implementation FLAnimatedImage
|
||||
|
||||
#pragma mark - Accessors
|
||||
#pragma mark Public
|
||||
|
||||
// This is the definite value the frame cache needs to size itself to.
|
||||
- (NSUInteger)frameCacheSizeCurrent
|
||||
{
|
||||
NSUInteger frameCacheSizeCurrent = self.frameCacheSizeOptimal;
|
||||
|
||||
// If set, respect the caps.
|
||||
if (self.frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit) {
|
||||
frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMax);
|
||||
}
|
||||
|
||||
if (self.frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit) {
|
||||
frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMaxInternal);
|
||||
}
|
||||
|
||||
return frameCacheSizeCurrent;
|
||||
}
|
||||
|
||||
|
||||
- (void)setFrameCacheSizeMax:(NSUInteger)frameCacheSizeMax
|
||||
{
|
||||
if (_frameCacheSizeMax != frameCacheSizeMax) {
|
||||
|
||||
// Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
|
||||
const BOOL willFrameCacheSizeShrink = (frameCacheSizeMax < self.frameCacheSizeCurrent);
|
||||
|
||||
// Update the value
|
||||
_frameCacheSizeMax = frameCacheSizeMax;
|
||||
|
||||
if (willFrameCacheSizeShrink) {
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
- (void)setFrameCacheSizeMaxInternal:(NSUInteger)frameCacheSizeMaxInternal
|
||||
{
|
||||
if (_frameCacheSizeMaxInternal != frameCacheSizeMaxInternal) {
|
||||
|
||||
// Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed.
|
||||
BOOL willFrameCacheSizeShrink = (frameCacheSizeMaxInternal < self.frameCacheSizeCurrent);
|
||||
|
||||
// Update the value
|
||||
_frameCacheSizeMaxInternal = frameCacheSizeMaxInternal;
|
||||
|
||||
if (willFrameCacheSizeShrink) {
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Life Cycle
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self == [FLAnimatedImage class]) {
|
||||
// UIKit memory warning notification handler shared by all of the instances
|
||||
allAnimatedImagesWeak = [NSHashTable weakObjectsHashTable];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
// UIKit notifications are posted on the main thread. didReceiveMemoryWarning: is expecting the main run loop, and we don't lock on allAnimatedImagesWeak
|
||||
NSAssert([NSThread isMainThread], @"Received memory warning on non-main thread");
|
||||
// Get a strong reference to all of the images. If an instance is returned in this array, it is still live and has not entered dealloc.
|
||||
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
|
||||
NSArray *images = nil;
|
||||
@synchronized(allAnimatedImagesWeak) {
|
||||
images = [[allAnimatedImagesWeak allObjects] copy];
|
||||
}
|
||||
// Now issue notifications to all of the images while holding a strong reference to them
|
||||
[images makeObjectsPerformSelector:@selector(didReceiveMemoryWarning:) withObject:note];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
FLAnimatedImage *_Nullable const animatedImage = [self initWithAnimatedGIFData:nil];
|
||||
if (!animatedImage) {
|
||||
FLLog(FLLogLevelError, @"Use `-initWithAnimatedGIFData:` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage`.");
|
||||
}
|
||||
return animatedImage;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data
|
||||
{
|
||||
return [self initWithAnimatedGIFData:data optimalFrameCacheSize:0 predrawingEnabled:YES];
|
||||
}
|
||||
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled
|
||||
{
|
||||
// Early return if no data supplied!
|
||||
const BOOL hasData = (data.length > 0);
|
||||
if (!hasData) {
|
||||
FLLog(FLLogLevelError, @"No animated GIF data supplied.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides.
|
||||
|
||||
// Keep a strong reference to `data` and expose it read-only publicly.
|
||||
// However, we will use the `_imageSource` as handler to the image data throughout our life cycle.
|
||||
_data = data;
|
||||
_predrawingEnabled = isPredrawingEnabled;
|
||||
|
||||
// Initialize internal data structures
|
||||
_cachedFramesForIndexes = [[NSMutableDictionary alloc] init];
|
||||
_cachedFrameIndexes = [[NSMutableIndexSet alloc] init];
|
||||
_requestedFrameIndexes = [[NSMutableIndexSet alloc] init];
|
||||
|
||||
// Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`.
|
||||
_imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
|
||||
(__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
|
||||
// Early return on failure!
|
||||
if (!_imageSource) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Early return if not GIF!
|
||||
const CFStringRef _Nullable imageSourceContainerType = CGImageSourceGetType(_imageSource);
|
||||
const BOOL isGIFData = imageSourceContainerType ? UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF) : NO;
|
||||
if (!isGIFData) {
|
||||
FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Get `LoopCount`
|
||||
// Note: 0 means repeating the animation indefinitely.
|
||||
// Image properties example:
|
||||
// {
|
||||
// FileSize = 314446;
|
||||
// "{GIF}" = {
|
||||
// HasGlobalColorMap = 1;
|
||||
// LoopCount = 0;
|
||||
// };
|
||||
// }
|
||||
NSDictionary *_Nullable const imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
|
||||
_loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
|
||||
|
||||
// Iterate through frame images
|
||||
const size_t imageCount = CGImageSourceGetCount(_imageSource);
|
||||
NSUInteger skippedFrameCount = 0;
|
||||
NSMutableDictionary *const delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
|
||||
for (size_t i = 0; i < imageCount; i++) {
|
||||
@autoreleasepool {
|
||||
const CGImageRef _Nullable frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
|
||||
if (frameImageRef) {
|
||||
UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
|
||||
// Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
|
||||
if (frameImage) {
|
||||
// Set poster image
|
||||
if (!self.posterImage) {
|
||||
_posterImage = frameImage;
|
||||
// Set its size to proxy our size.
|
||||
_size = _posterImage.size;
|
||||
// Remember index of poster image so we never purge it; also add it to the cache.
|
||||
_posterImageFrameIndex = i;
|
||||
[self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
|
||||
[self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
|
||||
}
|
||||
|
||||
// Get `DelayTime`
|
||||
// Note: It's not in (1/100) of a second like still falsely described in the documentation as per iOS 8 (rdar://19507384) but in seconds stored as `kCFNumberFloat32Type`.
|
||||
// Frame properties example:
|
||||
// {
|
||||
// ColorModel = RGB;
|
||||
// Depth = 8;
|
||||
// PixelHeight = 960;
|
||||
// PixelWidth = 640;
|
||||
// "{GIF}" = {
|
||||
// DelayTime = "0.4";
|
||||
// UnclampedDelayTime = "0.4";
|
||||
// };
|
||||
// }
|
||||
|
||||
NSDictionary *_Nullable const frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
|
||||
NSDictionary *_Nullable const framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
|
||||
|
||||
// Try to use the unclamped delay time; fall back to the normal delay time.
|
||||
NSNumber *_Nullable delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
|
||||
if (delayTime == nil) {
|
||||
delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
|
||||
}
|
||||
// If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value.
|
||||
const NSTimeInterval kDelayTimeIntervalDefault = 0.1;
|
||||
if (delayTime == nil) {
|
||||
if (i == 0) {
|
||||
FLLog(FLLogLevelInfo, @"Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties);
|
||||
delayTime = @(kDelayTimeIntervalDefault);
|
||||
} else {
|
||||
FLLog(FLLogLevelInfo, @"Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties);
|
||||
delayTime = delayTimesForIndexesMutable[@(i - 1)];
|
||||
}
|
||||
}
|
||||
// Support frame delays as low as `kFLAnimatedImageDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility.
|
||||
// To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO.
|
||||
if ([delayTime floatValue] < ((float)kFLAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) {
|
||||
FLLog(FLLogLevelInfo, @"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kFLAnimatedImageDelayTimeIntervalMinimum);
|
||||
delayTime = @(kDelayTimeIntervalDefault);
|
||||
}
|
||||
delayTimesForIndexesMutable[@(i)] = delayTime;
|
||||
} else {
|
||||
skippedFrameCount++;
|
||||
FLLog(FLLogLevelInfo, @"Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef);
|
||||
}
|
||||
CFRelease(frameImageRef);
|
||||
} else {
|
||||
skippedFrameCount++;
|
||||
FLLog(FLLogLevelInfo, @"Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, self->_imageSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
_delayTimesForIndexes = [delayTimesForIndexesMutable copy];
|
||||
_frameCount = imageCount;
|
||||
|
||||
if (self.frameCount == 0) {
|
||||
FLLog(FLLogLevelInfo, @"Failed to create any valid frames for GIF with properties %@", imageProperties);
|
||||
return nil;
|
||||
} else if (self.frameCount == 1) {
|
||||
// Warn when we only have a single frame but return a valid GIF.
|
||||
FLLog(FLLogLevelInfo, @"Created valid GIF but with only a single frame. Image properties: %@", imageProperties);
|
||||
} else {
|
||||
// We have multiple frames, rock on!
|
||||
}
|
||||
|
||||
// If no value is provided, select a default based on the GIF.
|
||||
if (optimalFrameCacheSize == 0) {
|
||||
// Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size.
|
||||
// It's only dependent on the image size & number of frames and never changes.
|
||||
const CGFloat animatedImageDataSize = (CGFloat)CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (CGFloat)(self.frameCount - skippedFrameCount) / (CGFloat)MEGABYTE;
|
||||
if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) {
|
||||
_frameCacheSizeOptimal = self.frameCount;
|
||||
} else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) {
|
||||
// This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames.
|
||||
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault;
|
||||
} else {
|
||||
// The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning.
|
||||
_frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory;
|
||||
}
|
||||
} else {
|
||||
// Use the provided value.
|
||||
_frameCacheSizeOptimal = optimalFrameCacheSize;
|
||||
}
|
||||
// In any case, cap the optimal cache size at the frame count.
|
||||
_frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount);
|
||||
|
||||
// Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`.
|
||||
_allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)];
|
||||
|
||||
// See the property declarations for descriptions.
|
||||
_weakProxy = (id)[FLWeakProxy weakProxyForObject:self];
|
||||
|
||||
// Register this instance in the weak table for memory notifications. The NSHashTable will clean up after itself when we're gone.
|
||||
// Note that FLAnimatedImages can be created on any thread, so the hash table must be locked.
|
||||
@synchronized(allAnimatedImagesWeak) {
|
||||
[allAnimatedImagesWeak addObject:self];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
+ (instancetype)animatedImageWithGIFData:(NSData *)data
|
||||
{
|
||||
FLAnimatedImage *const animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data];
|
||||
return animatedImage;
|
||||
}
|
||||
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_weakProxy) {
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:_weakProxy];
|
||||
}
|
||||
|
||||
if (_imageSource) {
|
||||
CFRelease(_imageSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
// See header for more details.
|
||||
// Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size).
|
||||
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index
|
||||
{
|
||||
// Early return if the requested index is beyond bounds.
|
||||
// Note: We're comparing an index with a count and need to bail on greater than or equal to.
|
||||
if (index >= self.frameCount) {
|
||||
FLLog(FLLogLevelWarn, @"Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Remember requested frame index, this influences what we should cache next.
|
||||
self.requestedFrameIndex = index;
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didRequestCachedFrame:)]) {
|
||||
[self.debug_delegate debug_animatedImage:self didRequestCachedFrame:index];
|
||||
}
|
||||
#endif
|
||||
|
||||
// Quick check to avoid doing any work if we already have all possible frames cached, a common case.
|
||||
if ([self.cachedFrameIndexes count] < self.frameCount) {
|
||||
// If we have frames that should be cached but aren't and aren't requested yet, request them.
|
||||
// Exclude existing cached frames, frames already requested, and specially cached poster image.
|
||||
NSMutableIndexSet *frameIndexesToAddToCacheMutable = [self frameIndexesToCache];
|
||||
[frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes];
|
||||
[frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes];
|
||||
[frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex];
|
||||
NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy];
|
||||
|
||||
// Asynchronously add frames to our cache.
|
||||
if ([frameIndexesToAddToCache count] > 0) {
|
||||
[self addFrameIndexesToCache:frameIndexesToAddToCache];
|
||||
}
|
||||
}
|
||||
|
||||
// Get the specified image.
|
||||
UIImage *const image = self.cachedFramesForIndexes[@(index)];
|
||||
|
||||
// Purge if needed based on the current playhead position.
|
||||
[self purgeFrameCacheIfNeeded];
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
// Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping.
|
||||
- (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache
|
||||
{
|
||||
// Order matters. First, iterate over the indexes starting from the requested frame index.
|
||||
// Then, if there are any indexes before the requested frame index, do those.
|
||||
const NSRange firstRange = NSMakeRange(self.requestedFrameIndex, self.frameCount - self.requestedFrameIndex);
|
||||
const NSRange secondRange = NSMakeRange(0, self.requestedFrameIndex);
|
||||
if (firstRange.length + secondRange.length != self.frameCount) {
|
||||
FLLog(FLLogLevelWarn, @"Two-part frame cache range doesn't equal full range.");
|
||||
}
|
||||
|
||||
// Add to the requested list before we actually kick them off, so they don't get into the queue twice.
|
||||
[self.requestedFrameIndexes addIndexes:frameIndexesToAddToCache];
|
||||
|
||||
// Lazily create dedicated isolation queue.
|
||||
if (!self.serialQueue) {
|
||||
_serialQueue = dispatch_queue_create("com.flipboard.framecachingqueue", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
|
||||
// Start streaming requested frames in the background into the cache.
|
||||
// Avoid capturing self in the block as there's no reason to keep doing work if the animated image went away.
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
// Produce and cache next needed frame.
|
||||
void (^frameRangeBlock)(NSRange, BOOL *) = ^(NSRange range, BOOL *stop) {
|
||||
// Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
|
||||
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
|
||||
#if defined(DEBUG) && DEBUG
|
||||
const CFTimeInterval predrawBeginTime = CACurrentMediaTime();
|
||||
#endif
|
||||
UIImage *const image = [weakSelf imageAtIndex:i];
|
||||
#if defined(DEBUG) && DEBUG
|
||||
const CFTimeInterval predrawDuration = CACurrentMediaTime() - predrawBeginTime;
|
||||
CFTimeInterval slowdownDuration = 0.0;
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImagePredrawingSlowdownFactor:)]) {
|
||||
CGFloat predrawingSlowdownFactor = [self.debug_delegate debug_animatedImagePredrawingSlowdownFactor:self];
|
||||
slowdownDuration = predrawDuration * predrawingSlowdownFactor - predrawDuration;
|
||||
[NSThread sleepForTimeInterval:slowdownDuration];
|
||||
}
|
||||
FLLog(FLLogLevelVerbose, @"Predrew frame %lu in %f ms for animated image: %@", (unsigned long)i, (predrawDuration + slowdownDuration) * 1000, self);
|
||||
#endif
|
||||
// The results get returned one by one as soon as they're ready (and not in batch).
|
||||
// The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy.
|
||||
if (image && weakSelf) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
weakSelf.cachedFramesForIndexes[@(i)] = image;
|
||||
[weakSelf.cachedFrameIndexes addIndex:i];
|
||||
[weakSelf.requestedFrameIndexes removeIndex:i];
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([weakSelf.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
|
||||
[weakSelf.debug_delegate debug_animatedImage:weakSelf didUpdateCachedFrames:weakSelf.cachedFrameIndexes];
|
||||
}
|
||||
#endif
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[frameIndexesToAddToCache enumerateRangesInRange:firstRange options:0 usingBlock:frameRangeBlock];
|
||||
[frameIndexesToAddToCache enumerateRangesInRange:secondRange options:0 usingBlock:frameRangeBlock];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+ (CGSize)sizeForImage:(id)image
|
||||
{
|
||||
CGSize imageSize = CGSizeZero;
|
||||
|
||||
// Early return for nil
|
||||
if (!image) {
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
if ([image isKindOfClass:[UIImage class]]) {
|
||||
UIImage *const uiImage = (UIImage *)image;
|
||||
imageSize = uiImage.size;
|
||||
} else if ([image isKindOfClass:[FLAnimatedImage class]]) {
|
||||
FLAnimatedImage *const animatedImage = (FLAnimatedImage *)image;
|
||||
imageSize = animatedImage.size;
|
||||
} else {
|
||||
// Bear trap to capture bad images; we have seen crashers cropping up on iOS 7.
|
||||
FLLog(FLLogLevelError, @"`image` isn't of expected types `UIImage` or `FLAnimatedImage`: %@", image);
|
||||
}
|
||||
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private Methods
|
||||
#pragma mark Frame Loading
|
||||
|
||||
- (UIImage *)imageAtIndex:(NSUInteger)index
|
||||
{
|
||||
// It's very important to use the cached `_imageSource` since the random access to a frame with `CGImageSourceCreateImageAtIndex` turns from an O(1) into an O(n) operation when re-initializing the image source every time.
|
||||
const CGImageRef _Nullable imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
|
||||
|
||||
// Early return for nil
|
||||
if (!imageRef) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithCGImage:imageRef];
|
||||
CFRelease(imageRef);
|
||||
|
||||
// Loading in the image object is only half the work, the displaying image view would still have to synchronosly wait and decode the image, so we go ahead and do that here on the background thread.
|
||||
if (self.isPredrawingEnabled) {
|
||||
image = [[self class] predrawnImageFromImage:image];
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Frame Caching
|
||||
|
||||
- (NSMutableIndexSet *)frameIndexesToCache
|
||||
{
|
||||
NSMutableIndexSet *indexesToCache = nil;
|
||||
// Quick check to avoid building the index set if the number of frames to cache equals the total frame count.
|
||||
if (self.frameCacheSizeCurrent == self.frameCount) {
|
||||
indexesToCache = [self.allFramesIndexSet mutableCopy];
|
||||
} else {
|
||||
indexesToCache = [[NSMutableIndexSet alloc] init];
|
||||
|
||||
// Add indexes to the set in two separate blocks- the first starting from the requested frame index, up to the limit or the end.
|
||||
// The second, if needed, the remaining number of frames beginning at index zero.
|
||||
const NSUInteger firstLength = MIN(self.frameCacheSizeCurrent, self.frameCount - self.requestedFrameIndex);
|
||||
const NSRange firstRange = NSMakeRange(self.requestedFrameIndex, firstLength);
|
||||
[indexesToCache addIndexesInRange:firstRange];
|
||||
const NSUInteger secondLength = self.frameCacheSizeCurrent - firstLength;
|
||||
if (secondLength > 0) {
|
||||
NSRange secondRange = NSMakeRange(0, secondLength);
|
||||
[indexesToCache addIndexesInRange:secondRange];
|
||||
}
|
||||
// Double check our math, before we add the poster image index which may increase it by one.
|
||||
if ([indexesToCache count] != self.frameCacheSizeCurrent) {
|
||||
FLLog(FLLogLevelWarn, @"Number of frames to cache doesn't equal expected cache size.");
|
||||
}
|
||||
|
||||
[indexesToCache addIndex:self.posterImageFrameIndex];
|
||||
}
|
||||
|
||||
return indexesToCache;
|
||||
}
|
||||
|
||||
|
||||
- (void)purgeFrameCacheIfNeeded
|
||||
{
|
||||
// Purge frames that are currently cached but don't need to be.
|
||||
// But not if we're still under the number of frames to cache.
|
||||
// This way, if all frames are allowed to be cached (the common case), we can skip all the `NSIndexSet` math below.
|
||||
if ([self.cachedFrameIndexes count] > self.frameCacheSizeCurrent) {
|
||||
NSMutableIndexSet *indexesToPurge = [self.cachedFrameIndexes mutableCopy];
|
||||
[indexesToPurge removeIndexes:[self frameIndexesToCache]];
|
||||
[indexesToPurge enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) {
|
||||
// Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`.
|
||||
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
|
||||
[self.cachedFrameIndexes removeIndex:i];
|
||||
[self.cachedFramesForIndexes removeObjectForKey:@(i)];
|
||||
// Note: Don't `CGImageSourceRemoveCacheAtIndex` on the image source for frames that we don't want cached any longer to maintain O(1) time access.
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.debug_delegate debug_animatedImage:self didUpdateCachedFrames:self.cachedFrameIndexes];
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)growFrameCacheSizeAfterMemoryWarning:(NSNumber *)frameCacheSize
|
||||
{
|
||||
self.frameCacheSizeMaxInternal = [frameCacheSize unsignedIntegerValue];
|
||||
FLLog(FLLogLevelDebug, @"Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self);
|
||||
|
||||
// Schedule resetting the frame cache size max completely after a while.
|
||||
const NSTimeInterval kResetDelay = 3.0;
|
||||
[self.weakProxy performSelector:@selector(resetFrameCacheSizeMaxInternal) withObject:nil afterDelay:kResetDelay];
|
||||
}
|
||||
|
||||
|
||||
- (void)resetFrameCacheSizeMaxInternal
|
||||
{
|
||||
self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit;
|
||||
FLLog(FLLogLevelDebug, @"Reset frame cache size max (current frame cache size: %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark System Memory Warnings Notification Handler
|
||||
|
||||
- (void)didReceiveMemoryWarning:(NSNotification *)notification
|
||||
{
|
||||
self.memoryWarningCount++;
|
||||
|
||||
// If we were about to grow larger, but got rapped on our knuckles by the system again, cancel.
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(growFrameCacheSizeAfterMemoryWarning:) object:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning)];
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(resetFrameCacheSizeMaxInternal) object:nil];
|
||||
|
||||
// Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on-demand.
|
||||
FLLog(FLLogLevelDebug, @"Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)self.frameCacheSizeMaxInternal, (unsigned long)self.memoryWarningCount, self);
|
||||
self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory;
|
||||
|
||||
// Schedule growing larger again after a while, but cap our attempts to prevent a periodic sawtooth wave (ramps upward and then sharply drops) of memory usage.
|
||||
//
|
||||
// [mem]^ (2) (5) (6) 1) Loading frames for the first time
|
||||
// (*)| , , , 2) Mem warning #1; purge cache
|
||||
// | /| (4)/| /| 3) Grow cache size a bit after a while, if no mem warning occurs
|
||||
// | / | _/ | _/ | 4) Try to grow cache size back to optimum after a while, if no mem warning occurs
|
||||
// |(1)/ |_/ |/ |__(7) 5) Mem warning #2; purge cache
|
||||
// |__/ (3) 6) After repetition of (3) and (4), mem warning #3; purge cache
|
||||
// +----------------------> 7) After 3 mem warnings, stay at minimum cache size
|
||||
// [t]
|
||||
// *) The mem high water mark before we get warned might change for every cycle.
|
||||
//
|
||||
const NSUInteger kGrowAttemptsMax = 2;
|
||||
const NSTimeInterval kGrowDelay = 2.0;
|
||||
if ((self.memoryWarningCount - 1) <= kGrowAttemptsMax) {
|
||||
[self.weakProxy performSelector:@selector(growFrameCacheSizeAfterMemoryWarning:) withObject:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning) afterDelay:kGrowDelay];
|
||||
}
|
||||
|
||||
// Note: It's not possible to get the level of a memory warning with a public API: http://stackoverflow.com/questions/2915247/iphone-os-memory-warnings-what-do-the-different-levels-mean/2915477#2915477
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Image Decoding
|
||||
|
||||
// Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread.
|
||||
// On success, the returned object is a new `UIImage` instance with the same content as the one passed in.
|
||||
// On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged.
|
||||
// First inspired by & good Karma to: https://gist.github.com/steipete/1144242
|
||||
+ (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw
|
||||
{
|
||||
// Always use a device RGB color space for simplicity and predictability what will be going on.
|
||||
const CGColorSpaceRef _Nullable colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB();
|
||||
// Early return on failure!
|
||||
if (!colorSpaceDeviceRGBRef) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
// Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB:
|
||||
// kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast
|
||||
// (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts")
|
||||
const size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A
|
||||
|
||||
// "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs)
|
||||
void *_Nullable data = NULL;
|
||||
const size_t width = imageToPredraw.size.width;
|
||||
const size_t height = imageToPredraw.size.height;
|
||||
const size_t bitsPerComponent = CHAR_BIT;
|
||||
|
||||
const size_t bitsPerPixel = (bitsPerComponent * numberOfComponents);
|
||||
const size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE);
|
||||
const size_t bytesPerRow = (bytesPerPixel * width);
|
||||
|
||||
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
|
||||
|
||||
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage);
|
||||
// If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.
|
||||
// "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs)
|
||||
if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) {
|
||||
alphaInfo = kCGImageAlphaNoneSkipFirst;
|
||||
} else if (alphaInfo == kCGImageAlphaFirst) {
|
||||
alphaInfo = kCGImageAlphaPremultipliedFirst;
|
||||
} else if (alphaInfo == kCGImageAlphaLast) {
|
||||
alphaInfo = kCGImageAlphaPremultipliedLast;
|
||||
}
|
||||
// "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs)
|
||||
bitmapInfo |= alphaInfo;
|
||||
|
||||
// Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time).
|
||||
// Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why.
|
||||
const CGContextRef _Nullable bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo);
|
||||
CGColorSpaceRelease(colorSpaceDeviceRGBRef);
|
||||
// Early return on failure!
|
||||
if (!bitmapContextRef) {
|
||||
FLLog(FLLogLevelError, @"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
// Draw image in bitmap context and create image by preserving receiver's properties.
|
||||
CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage);
|
||||
const CGImageRef _Nullable predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef);
|
||||
UIImage *_Nullable predrawnImage = predrawnImageRef ? [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation] : nil;
|
||||
CGImageRelease(predrawnImageRef);
|
||||
CGContextRelease(bitmapContextRef);
|
||||
|
||||
// Early return on failure!
|
||||
if (!predrawnImage) {
|
||||
FLLog(FLLogLevelError, @"Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw);
|
||||
return imageToPredraw;
|
||||
}
|
||||
|
||||
return predrawnImage;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Description
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *description = [super description];
|
||||
|
||||
description = [description stringByAppendingFormat:@" size=%@", NSStringFromCGSize(self.size)];
|
||||
description = [description stringByAppendingFormat:@" frameCount=%lu", (unsigned long)self.frameCount];
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
@implementation FLAnimatedImage (Logging)
|
||||
|
||||
static void (^_logBlock)(NSString *logString, FLLogLevel logLevel) = nil;
|
||||
static FLLogLevel _logLevel;
|
||||
|
||||
+ (void)setLogBlock:(void (^_Nullable)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel
|
||||
{
|
||||
_logBlock = [logBlock copy];
|
||||
_logLevel = logLevel;
|
||||
}
|
||||
|
||||
+ (void)logStringFromBlock:(NSString *(^_Nullable)(void))stringBlock withLevel:(FLLogLevel)level
|
||||
{
|
||||
if (level <= _logLevel && _logBlock && stringBlock) {
|
||||
_logBlock(stringBlock(), level);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - FLWeakProxy
|
||||
|
||||
@interface FLWeakProxy ()
|
||||
|
||||
@property (nonatomic, weak) id target;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLWeakProxy
|
||||
|
||||
#pragma mark Life Cycle
|
||||
|
||||
// This is the designated creation method of an `FLWeakProxy` and
|
||||
// as a subclass of `NSProxy` it doesn't respond to or need `-init`.
|
||||
+ (instancetype)weakProxyForObject:(id)targetObject
|
||||
{
|
||||
FLWeakProxy *weakProxy = [FLWeakProxy alloc];
|
||||
weakProxy.target = targetObject;
|
||||
return weakProxy;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Forwarding Messages
|
||||
|
||||
- (id)forwardingTargetForSelector:(SEL)selector
|
||||
{
|
||||
// Keep it lightweight: access the ivar directly
|
||||
return _target;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - NSWeakProxy Method Overrides
|
||||
#pragma mark Handling Unimplemented Methods
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
{
|
||||
// Fallback for when target is nil. Don't do anything, just return 0/NULL/nil.
|
||||
// The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing.
|
||||
// We can't really handle struct return types here because we don't know the length.
|
||||
void *_Nullable nullPointer = NULL;
|
||||
[invocation setReturnValue:&nullPointer];
|
||||
}
|
||||
|
||||
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
|
||||
{
|
||||
// We only get here if `forwardingTargetForSelector:` returns nil.
|
||||
// In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing.
|
||||
// We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`.
|
||||
// Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well.
|
||||
// See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache.
|
||||
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
460
Pods/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.m
generated
Executable file
460
Pods/FLAnimatedImage/FLAnimatedImage/FLAnimatedImageView.m
generated
Executable file
@@ -0,0 +1,460 @@
|
||||
//
|
||||
// FLAnimatedImageView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "FLAnimatedImageView.h"
|
||||
#import "FLAnimatedImage.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@protocol FLAnimatedImageViewDebugDelegate <NSObject>
|
||||
@optional
|
||||
- (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration;
|
||||
@end
|
||||
#endif
|
||||
|
||||
|
||||
@interface FLAnimatedImageView ()
|
||||
|
||||
// Override of public `readonly` properties as private `readwrite`
|
||||
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
|
||||
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
|
||||
|
||||
@property (nonatomic, assign) NSUInteger loopCountdown;
|
||||
@property (nonatomic, assign) NSTimeInterval accumulator;
|
||||
@property (nonatomic, strong) CADisplayLink *displayLink;
|
||||
|
||||
@property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image or visibility (window, superview, hidden, alpha) has changed.
|
||||
@property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
|
||||
|
||||
#if defined(DEBUG) && DEBUG
|
||||
@property (nonatomic, weak) id<FLAnimatedImageViewDebugDelegate> debug_delegate;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLAnimatedImageView
|
||||
@synthesize runLoopMode = _runLoopMode;
|
||||
|
||||
#pragma mark - Initializers
|
||||
|
||||
// -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be.
|
||||
// Using -initWithImage: doesn't call any of the other designated initializers.
|
||||
- (instancetype)initWithImage:(UIImage *)image
|
||||
{
|
||||
self = [super initWithImage:image];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers.
|
||||
- (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage
|
||||
{
|
||||
self = [super initWithImage:image highlightedImage:highlightedImage];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
self.runLoopMode = [[self class] defaultRunLoopMode];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.accessibilityIgnoresInvertColors = YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
#pragma mark Public
|
||||
|
||||
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
|
||||
{
|
||||
if (![_animatedImage isEqual:animatedImage]) {
|
||||
if (animatedImage) {
|
||||
if (super.image) {
|
||||
// UIImageView's `setImage:` will internally call its layer's `setContentsTransform:` based on the `image.imageOrientation`.
|
||||
// The `contentsTransform` will affect layer rendering rotation because the CGImage's bitmap buffer does not actually take rotation.
|
||||
// However, when calling `setImage:nil`, this `contentsTransform` will not be reset to identity.
|
||||
// Further animation frame will be rendered as rotated. So we must set it to the poster image to clear the previous state.
|
||||
// See more here: https://github.com/Flipboard/FLAnimatedImage/issues/100
|
||||
super.image = animatedImage.posterImage;
|
||||
// Clear out the image.
|
||||
super.image = nil;
|
||||
}
|
||||
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
|
||||
super.highlighted = NO;
|
||||
// UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
|
||||
[self invalidateIntrinsicContentSize];
|
||||
} else {
|
||||
// Stop animating before the animated image gets cleared out.
|
||||
[self stopAnimating];
|
||||
}
|
||||
|
||||
_animatedImage = animatedImage;
|
||||
|
||||
self.currentFrame = animatedImage.posterImage;
|
||||
self.currentFrameIndex = 0;
|
||||
if (animatedImage.loopCount > 0) {
|
||||
self.loopCountdown = animatedImage.loopCount;
|
||||
} else {
|
||||
self.loopCountdown = NSUIntegerMax;
|
||||
}
|
||||
self.accumulator = 0.0;
|
||||
|
||||
// Start animating after the new animated image has been set.
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
}
|
||||
|
||||
[self.layer setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Life Cycle
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// Removes the display link from all run loop modes.
|
||||
[_displayLink invalidate];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIView Method Overrides
|
||||
#pragma mark Observing View-Related Changes
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAlpha:(CGFloat)alpha
|
||||
{
|
||||
[super setAlpha:alpha];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden
|
||||
{
|
||||
[super setHidden:hidden];
|
||||
|
||||
[self updateShouldAnimate];
|
||||
if (self.shouldAnimate) {
|
||||
[self startAnimating];
|
||||
} else {
|
||||
[self stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Auto Layout
|
||||
|
||||
- (CGSize)intrinsicContentSize
|
||||
{
|
||||
// Default to let UIImageView handle the sizing of its image, and anything else it might consider.
|
||||
CGSize intrinsicContentSize = [super intrinsicContentSize];
|
||||
|
||||
// If we have have an animated image, use its image size.
|
||||
// UIImageView's intrinsic content size seems to be the size of its image. The obvious approach, simply calling `-invalidateIntrinsicContentSize` when setting an animated image, results in UIImageView steadfastly returning `{UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric}` for its intrinsicContentSize.
|
||||
// (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.)
|
||||
if (self.animatedImage) {
|
||||
intrinsicContentSize = self.image.size;
|
||||
}
|
||||
|
||||
return intrinsicContentSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIImageView Method Overrides
|
||||
#pragma mark Image Data
|
||||
|
||||
- (UIImage *)image
|
||||
{
|
||||
UIImage *image = nil;
|
||||
if (self.animatedImage) {
|
||||
// Initially set to the poster image.
|
||||
image = self.currentFrame;
|
||||
} else {
|
||||
image = super.image;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
- (void)setImage:(UIImage *)image
|
||||
{
|
||||
if (image) {
|
||||
// Clear out the animated image and implicitly pause animation playback.
|
||||
self.animatedImage = nil;
|
||||
}
|
||||
|
||||
super.image = image;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Animating Images
|
||||
|
||||
- (NSTimeInterval)frameDelayGreatestCommonDivisor
|
||||
{
|
||||
// Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping.
|
||||
const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum;
|
||||
|
||||
NSArray *const delays = self.animatedImage.delayTimesForIndexes.allValues;
|
||||
|
||||
// Scales the frame delays by `kGreatestCommonDivisorPrecision`
|
||||
// then converts it to an UInteger for in order to calculate the GCD.
|
||||
NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision);
|
||||
for (NSNumber *value in delays) {
|
||||
scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD);
|
||||
}
|
||||
|
||||
// Reverse to scale to get the value back into seconds.
|
||||
return (double)scaledGCD / kGreatestCommonDivisorPrecision;
|
||||
}
|
||||
|
||||
|
||||
static NSUInteger gcd(NSUInteger a, NSUInteger b)
|
||||
{
|
||||
// http://en.wikipedia.org/wiki/Greatest_common_divisor
|
||||
if (a < b) {
|
||||
return gcd(b, a);
|
||||
} else if (a == b) {
|
||||
return b;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const NSUInteger remainder = a % b;
|
||||
if (remainder == 0) {
|
||||
return b;
|
||||
}
|
||||
a = b;
|
||||
b = remainder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)startAnimating
|
||||
{
|
||||
if (self.animatedImage) {
|
||||
// Lazily create the display link.
|
||||
if (!self.displayLink) {
|
||||
// It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
|
||||
// will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
|
||||
// independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
|
||||
// link which will lead to the deallocation of both the display link and the weak proxy.
|
||||
FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
|
||||
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
|
||||
|
||||
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
|
||||
}
|
||||
|
||||
if (@available(iOS 10, *)) {
|
||||
// Adjusting preferredFramesPerSecond allows us to skip unnecessary calls to displayDidRefresh: when showing GIFs
|
||||
// that don't animate quickly. Use ceil to err on the side of too many FPS so we don't miss a frame transition moment.
|
||||
self.displayLink.preferredFramesPerSecond = ceil(1.0 / [self frameDelayGreatestCommonDivisor]);
|
||||
} else {
|
||||
const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
|
||||
self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
|
||||
}
|
||||
self.displayLink.paused = NO;
|
||||
} else {
|
||||
[super startAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setRunLoopMode:(NSRunLoopMode)runLoopMode
|
||||
{
|
||||
if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) {
|
||||
NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode);
|
||||
_runLoopMode = [[self class] defaultRunLoopMode];
|
||||
} else {
|
||||
_runLoopMode = runLoopMode;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopAnimating
|
||||
{
|
||||
if (self.animatedImage) {
|
||||
self.displayLink.paused = YES;
|
||||
} else {
|
||||
[super stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)isAnimating
|
||||
{
|
||||
BOOL isAnimating = NO;
|
||||
if (self.animatedImage) {
|
||||
isAnimating = self.displayLink && !self.displayLink.isPaused;
|
||||
} else {
|
||||
isAnimating = [super isAnimating];
|
||||
}
|
||||
return isAnimating;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Highlighted Image Unsupport
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
// Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell.
|
||||
if (!self.animatedImage) {
|
||||
[super setHighlighted:highlighted];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private Methods
|
||||
#pragma mark Animation
|
||||
|
||||
// Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons.
|
||||
// Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed.
|
||||
- (void)updateShouldAnimate
|
||||
{
|
||||
const BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
|
||||
self.shouldAnimate = self.animatedImage && isVisible;
|
||||
}
|
||||
|
||||
|
||||
- (void)displayDidRefresh:(CADisplayLink *)displayLink
|
||||
{
|
||||
// If for some reason a wild call makes it through when we shouldn't be animating, bail.
|
||||
// Early return!
|
||||
if (!self.shouldAnimate) {
|
||||
FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self);
|
||||
return;
|
||||
}
|
||||
|
||||
NSNumber *_Nullable const delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
|
||||
// If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
|
||||
if (delayTimeNumber != nil) {
|
||||
const NSTimeInterval delayTime = [delayTimeNumber floatValue];
|
||||
// If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
|
||||
UIImage *_Nullable const image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
|
||||
if (image) {
|
||||
FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
|
||||
self.currentFrame = image;
|
||||
if (self.needsDisplayWhenImageBecomesAvailable) {
|
||||
[self.layer setNeedsDisplay];
|
||||
self.needsDisplayWhenImageBecomesAvailable = NO;
|
||||
}
|
||||
|
||||
if (@available(iOS 10, *)) {
|
||||
self.accumulator += displayLink.targetTimestamp - CACurrentMediaTime();
|
||||
} else {
|
||||
self.accumulator += displayLink.duration * (NSTimeInterval)displayLink.frameInterval;
|
||||
}
|
||||
|
||||
// While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
|
||||
while (self.accumulator >= delayTime) {
|
||||
self.accumulator -= delayTime;
|
||||
self.currentFrameIndex++;
|
||||
if (self.currentFrameIndex >= self.animatedImage.frameCount) {
|
||||
// If we've looped the number of times that this animated image describes, stop looping.
|
||||
self.loopCountdown--;
|
||||
if (self.loopCompletionBlock) {
|
||||
self.loopCompletionBlock(self.loopCountdown);
|
||||
}
|
||||
|
||||
if (self.loopCountdown == 0) {
|
||||
[self stopAnimating];
|
||||
return;
|
||||
}
|
||||
self.currentFrameIndex = 0;
|
||||
}
|
||||
// Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
|
||||
// Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
|
||||
self.needsDisplayWhenImageBecomesAvailable = YES;
|
||||
}
|
||||
} else {
|
||||
FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
|
||||
#if defined(DEBUG) && DEBUG
|
||||
if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) {
|
||||
if (@available(iOS 10, *)) {
|
||||
[self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:displayLink.targetTimestamp - CACurrentMediaTime()];
|
||||
} else {
|
||||
[self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:displayLink.duration * (NSTimeInterval)displayLink.frameInterval];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
self.currentFrameIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSRunLoopMode)defaultRunLoopMode
|
||||
{
|
||||
// Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations.
|
||||
return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - CALayerDelegate (Informal)
|
||||
#pragma mark Providing the Layer's Content
|
||||
|
||||
- (void)displayLayer:(CALayer *)layer
|
||||
{
|
||||
layer.contents = (__bridge id)self.image.CGImage;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
83
Pods/FLAnimatedImage/FLAnimatedImage/include/FLAnimatedImage.h
generated
Normal file
83
Pods/FLAnimatedImage/FLAnimatedImage/include/FLAnimatedImage.h
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// FLAnimatedImage.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// Allow user classes conveniently just importing one header.
|
||||
#import "FLAnimatedImageView.h"
|
||||
|
||||
#ifndef NS_DESIGNATED_INITIALIZER
|
||||
#if __has_attribute(objc_designated_initializer)
|
||||
#define NS_DESIGNATED_INITIALIZER __attribute((objc_designated_initializer))
|
||||
#else
|
||||
#define NS_DESIGNATED_INITIALIZER
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extern const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum;
|
||||
|
||||
//
|
||||
// An `FLAnimatedImage`'s job is to deliver frames in a highly performant way and works in conjunction with `FLAnimatedImageView`.
|
||||
// It subclasses `NSObject` and not `UIImage` because it's only an "image" in the sense that a sea lion is a lion.
|
||||
// It tries to intelligently choose the frame cache size depending on the image and memory situation with the goal to lower CPU usage for smaller ones, lower memory usage for larger ones and always deliver frames for high performant play-back.
|
||||
// Note: `posterImage`, `size`, `loopCount`, `delayTimes` and `frameCount` don't change after successful initialization.
|
||||
//
|
||||
@interface FLAnimatedImage : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) UIImage *posterImage; // Guaranteed to be loaded; usually equivalent to `-imageLazilyCachedAtIndex:0`
|
||||
@property (nonatomic, assign, readonly) CGSize size; // The `.posterImage`'s `.size`
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger loopCount; // "The number of times to repeat an animated sequence." according to ImageIO (note the slightly different definition to Netscape 2.0 Loop Extension); 0 means repeating the animation forever
|
||||
@property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // Of type `NSTimeInterval` boxed in `NSNumber`s
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCount; // Number of valid frames; equal to `[.delayTimes count]`
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; // Current size of intelligently chosen buffer window; can range in the interval [1..frameCount]
|
||||
@property (nonatomic, assign) NSUInteger frameCacheSizeMax; // Allow to cap the cache size; 0 means no specific limit (default)
|
||||
|
||||
// Intended to be called from main thread synchronously; will return immediately.
|
||||
// If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling.
|
||||
// After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache.
|
||||
- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index;
|
||||
|
||||
// Pass either a `UIImage` or an `FLAnimatedImage` and get back its size
|
||||
+ (CGSize)sizeForImage:(id)image;
|
||||
|
||||
// On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged.
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data;
|
||||
// Pass 0 for optimalFrameCacheSize to get the default, predrawing is enabled by default.
|
||||
- (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled NS_DESIGNATED_INITIALIZER;
|
||||
+ (instancetype)animatedImageWithGIFData:(NSData *)data;
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only
|
||||
|
||||
@end
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLLogLevel) {
|
||||
FLLogLevelNone = 0,
|
||||
FLLogLevelError,
|
||||
FLLogLevelWarn,
|
||||
FLLogLevelInfo,
|
||||
FLLogLevelDebug,
|
||||
FLLogLevelVerbose
|
||||
};
|
||||
|
||||
@interface FLAnimatedImage (Logging)
|
||||
|
||||
+ (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel;
|
||||
+ (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level;
|
||||
|
||||
@end
|
||||
|
||||
#define FLLog(logLevel, format, ...) [FLAnimatedImage logStringFromBlock:^NSString *{ return [NSString stringWithFormat:(format), ## __VA_ARGS__]; } withLevel:(logLevel)]
|
||||
|
||||
@interface FLWeakProxy : NSProxy
|
||||
|
||||
+ (instancetype)weakProxyForObject:(id)targetObject;
|
||||
|
||||
@end
|
||||
36
Pods/FLAnimatedImage/FLAnimatedImage/include/FLAnimatedImageView.h
generated
Normal file
36
Pods/FLAnimatedImage/FLAnimatedImage/include/FLAnimatedImageView.h
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// FLAnimatedImageView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Raphael Schaad on 7/8/13.
|
||||
// Copyright (c) Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLAnimatedImage;
|
||||
@protocol FLAnimatedImageViewDebugDelegate;
|
||||
|
||||
|
||||
//
|
||||
// An `FLAnimatedImageView` can take an `FLAnimatedImage` and plays it automatically when in view hierarchy and stops when removed.
|
||||
// The animation can also be controlled with the `UIImageView` methods `-start/stop/isAnimating`.
|
||||
// It is a fully compatible `UIImageView` subclass and can be used as a drop-in component to work with existing code paths expecting to display a `UIImage`.
|
||||
// Under the hood it uses a `CADisplayLink` for playback, which can be inspected with `currentFrame` & `currentFrameIndex`.
|
||||
//
|
||||
@interface FLAnimatedImageView : UIImageView
|
||||
|
||||
// Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`.
|
||||
// And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`.
|
||||
@property (nonatomic, strong) FLAnimatedImage *animatedImage;
|
||||
@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
|
||||
|
||||
@property (nonatomic, strong, readonly) UIImage *currentFrame;
|
||||
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
|
||||
|
||||
// The animation runloop mode. Enables playback during scrolling by allowing timer events (i.e. animation) with NSRunLoopCommonModes.
|
||||
// To keep scrolling smooth on single-core devices such as iPhone 3GS/4 and iPod Touch 4th gen, the default run loop mode is NSDefaultRunLoopMode. Otherwise, the default is NSDefaultRunLoopMode.
|
||||
@property (nonatomic, copy) NSRunLoopMode runLoopMode;
|
||||
|
||||
@end
|
||||
21
Pods/FLAnimatedImage/LICENSE
generated
Normal file
21
Pods/FLAnimatedImage/LICENSE
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2016 Flipboard
|
||||
|
||||
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.
|
||||
122
Pods/FLAnimatedImage/README.md
generated
Normal file
122
Pods/FLAnimatedImage/README.md
generated
Normal file
@@ -0,0 +1,122 @@
|
||||
# [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) · [](https://github.com/Flipboard/FLAnimatedImage/blob/master/LICENSE) [](https://github.com/Flipboard/FLAnimatedImage/pulls)
|
||||
|
||||
FLAnimatedImage is a performant animated GIF engine for iOS:
|
||||
|
||||
- Plays multiple GIFs simultaneously with a playback speed comparable to desktop browsers
|
||||
- Honors variable frame delays
|
||||
- Behaves gracefully under memory pressure
|
||||
- Eliminates delays or blocking during the first playback loop
|
||||
- Interprets the frame delays of fast GIFs the same way modern browsers do
|
||||
|
||||
It's a well-tested [component that powers all GIFs in Flipboard](http://engineering.flipboard.com/2014/05/animated-gif). To understand its behavior it comes with an interactive demo:
|
||||
|
||||

|
||||
|
||||
## Who is this for?
|
||||
|
||||
- Apps that don't support animated GIFs yet
|
||||
- Apps that already support animated GIFs but want a higher performance solution
|
||||
- People who want to tinker with the code ([the corresponding blog post](http://engineering.flipboard.com/2014/05/animated-gif/) is a great place to start; also see the *To Do* section below)
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
FLAnimatedImage is a well-encapsulated drop-in component. Simply replace your `UIImageView` instances with instances of `FLAnimatedImageView` to get animated GIF support. There is no central cache or state to manage.
|
||||
|
||||
If using CocoaPods, the quickest way to try it out is to type this on the command line:
|
||||
|
||||
```shell
|
||||
$ pod try FLAnimatedImage
|
||||
```
|
||||
|
||||
To add it to your app, copy the two classes `FLAnimatedImage.h/.m` and `FLAnimatedImageView.h/.m` into your Xcode project or add via [CocoaPods](http://cocoapods.org) by adding this to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'FLAnimatedImage', '~> 1.0'
|
||||
```
|
||||
|
||||
If using [Carthage](https://github.com/Carthage/Carthage), add the following line into your `Cartfile`
|
||||
|
||||
```
|
||||
github "Flipboard/FLAnimatedImage"
|
||||
```
|
||||
|
||||
If using [Swift Package Manager](https://github.com/apple/swift-package-manager), add the following to your `Package.swift` or add via XCode:
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Flipboard/FLAnimatedImage.git", .upToNextMajor(from: "1.0.16"))
|
||||
],
|
||||
targets: [
|
||||
.target(name: "TestProject", dependencies: ["FLAnimatedImage""])
|
||||
]
|
||||
```
|
||||
|
||||
In your code, `#import "FLAnimatedImage.h"`, create an image from an animated GIF, and setup the image view to display it:
|
||||
|
||||
```objective-c
|
||||
FLAnimatedImage *image = [FLAnimatedImage animatedImageWithGIFData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif"]]];
|
||||
FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init];
|
||||
imageView.animatedImage = image;
|
||||
imageView.frame = CGRectMake(0.0, 0.0, 100.0, 100.0);
|
||||
[self.view addSubview:imageView];
|
||||
```
|
||||
|
||||
It's flexible to integrate in your custom image loading stack and backwards compatible to iOS 9.
|
||||
|
||||
It uses ARC and the Apple frameworks `QuartzCore`, `ImageIO`, `MobileCoreServices`, and `CoreGraphics`.
|
||||
|
||||
It is capable of fine-grained logging. A block can be set on `FLAnimatedImage` that's invoked when logging occurs with various log levels via the `+setLogBlock:logLevel:` method. For example:
|
||||
|
||||
```objective-c
|
||||
// Set up FLAnimatedImage logging.
|
||||
[FLAnimatedImage setLogBlock:^(NSString *logString, FLLogLevel logLevel) {
|
||||
// Using NSLog
|
||||
NSLog(@"%@", logString);
|
||||
|
||||
// ...or CocoaLumberjackLogger only logging warnings and errors
|
||||
if (logLevel == FLLogLevelError) {
|
||||
DDLogError(@"%@", logString);
|
||||
} else if (logLevel == FLLogLevelWarn) {
|
||||
DDLogWarn(@"%@", logString);
|
||||
}
|
||||
} logLevel:FLLogLevelWarn];
|
||||
```
|
||||
|
||||
Since FLAnimatedImage is licensed under MIT, it's compatible with the terms of using it for any app on the App Store.
|
||||
|
||||
## Release process
|
||||
1. Bump version in `FLAnimatedImage.podspec`, update CHANGES, and commit.
|
||||
2. Tag commit with `> git tag -a <VERSION> -m "<VERSION>"` and `> git push --tags`.
|
||||
3. [Submit Podspec to Trunk with](https://guides.cocoapods.org/making/specs-and-specs-repo.html#how-do-i-update-an-existing-pod) `> pod trunk push FLAnimatedImage.podspec` ([ensure you're auth'ed](https://guides.cocoapods.org/making/getting-setup-with-trunk.html#getting-started)).
|
||||
## To Do
|
||||
- Support other animated image formats such as APNG or WebP (WebP support implemented [here](https://github.com/Flipboard/FLAnimatedImage/pull/86))
|
||||
- Integration into network libraries and image caches
|
||||
- Investigate whether `FLAnimatedImage` should become a `UIImage` subclass
|
||||
- Smarter buffering
|
||||
- Bring demo app to iPhone
|
||||
|
||||
This code has successfully shipped to many people as is, but please do come with your questions, issues and pull requests!
|
||||
|
||||
## Select apps using FLAnimatedImage
|
||||
(alphabetically)
|
||||
|
||||
- [Close-up](http://closeu.pe)
|
||||
- [Design Shots](https://itunes.apple.com/app/id792517951)
|
||||
- [Dropbox](https://www.dropbox.com)
|
||||
- [Dumpert](http://dumpert.nl)
|
||||
- [Ello](https://ello.co/)
|
||||
- [Facebook](https://facebook.com)
|
||||
- [Flipboard](https://flipboard.com)
|
||||
- [getGIF](https://itunes.apple.com/app/id964784701)
|
||||
- [Gifalicious](https://itunes.apple.com/us/app/gifalicious-see-your-gifs/id965346708?mt=8)
|
||||
- [HashPhotos](https://itunes.apple.com/app/id685784609)
|
||||
- [Instagram](https://www.instagram.com/)
|
||||
- [LiveBooth](http://www.liveboothapp.com)
|
||||
- [lWlVl Festival](http://lwlvl.com)
|
||||
- [Medium](https://medium.com)
|
||||
- [Pinterest](https://pinterest.com)
|
||||
- [Slack](https://slack.com/)
|
||||
- [Telegram](https://telegram.org/)
|
||||
- [Zip Code Finder](https://itunes.apple.com/app/id893031254)
|
||||
|
||||
If you're using FLAnimatedImage in your app, please open a PR to add it to this list!
|
||||
21
Pods/HWPanModal/LICENSE
generated
Normal file
21
Pods/HWPanModal/LICENSE
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Heath Wang
|
||||
|
||||
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.
|
||||
330
Pods/HWPanModal/README-CN.md
generated
Normal file
330
Pods/HWPanModal/README-CN.md
generated
Normal file
@@ -0,0 +1,330 @@
|
||||
|
||||
# HWPanModal 👍
|
||||
<p style="align: left">
|
||||
<a href="https://cocoapods.org/pods/HWPanModal">
|
||||
<img src="https://img.shields.io/cocoapods/v/HWPanModal.svg?style=flat">
|
||||
</a>
|
||||
<a href="https://cocoapods.org/pods/HWPanModal">
|
||||
<img src="https://img.shields.io/cocoapods/p/HWPanModal.svg?style=flat">
|
||||
</a>
|
||||
<a href="https://cocoapods.org/pods/HWPanModal">
|
||||
<img src="https://img.shields.io/badge/support-ios%208%2B-orange.svg">
|
||||
</a>
|
||||
<a href="https://cocoapods.org/pods/HWPanModal">
|
||||
<img src="https://img.shields.io/badge/language-objective--c-blue.svg">
|
||||
</a>
|
||||
<a href="https://travis-ci.org/HeathWang/HWPanModal">
|
||||
<img src="https://travis-ci.org/HeathWang/HWPanModal.svg?branch=master">
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-heathwang-hwpanmodal-master">
|
||||
<img alt="codebeat badge" src="https://codebeat.co/badges/fb96e7ea-2320-4219-8f19-777674a97d0e" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
HWPanModal 用于从底部弹出控制器(UIViewController),并用拖拽手势来关闭控制器。提供了自定义视图大小和位置,高度自定义弹出视图的各个属性。
|
||||
|
||||
APP中常见的从底部弹出视图,可以通过该框架快速实现,只需专注于相应的视图编写。常规热门app的UI示例:
|
||||
1. 知乎APP的查看评论
|
||||
2. 抖音的评论查看
|
||||
3. 微信,网易音乐等app弹出分享
|
||||
4. 嘀嗒出行行程进行页(地图上的浮层view效果)
|
||||
5. iOS13 默认模态(present)效果
|
||||
6. And more...
|
||||
|
||||
## 特别感谢
|
||||
|
||||
Special Thanks to JetBrains! I use AppCode IDE to develop my open source project.
|
||||
<p style="align: left">
|
||||
<a href="https://www.jetbrains.com/?from=HWPanModal">
|
||||
<img src="images/icon_AppCode.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 截图
|
||||
|
||||
<div style="text-align: center">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Basic</th>
|
||||
<th>Blur background</th>
|
||||
<th>Keyboard handle</th>
|
||||
<th>App demo</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: center">
|
||||
<img src="images/HWPanModal_example.gif" width="180" />
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<img src="images/HWPanModal_example_3.gif" width="180"/>
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<img src="images/HWPanModal_example_4.gif" width="180"/>
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<img src="images/HWPanModal_example_2.gif" width="180"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## 功能
|
||||
1. 支持任意类型的 `UIViewController`
|
||||
2. 支持继承自 `HWPanModalContentView` 的view
|
||||
3. 平滑的转场动画
|
||||
4. 支持2种类型的手势dismiss视图
|
||||
1. 上下方向拖动关闭视图。
|
||||
2. 侧滑关闭视图,支持全屏侧滑。
|
||||
5. 支持为presenting VC编写自定义动画。
|
||||
6. 支持配置动画时间,动画options,弹性spring值
|
||||
7. 支持配置背景alpha,或者高斯模糊背景。注意:动态调整模糊效果仅工作于iOS9.0+。
|
||||
8. 支持显示隐藏指示器,修改圆角
|
||||
9. 自动处理键盘弹出消失事件。
|
||||
10. 自定义指示器indicator view。
|
||||
11. 事件可以穿透到下层presenting VC。
|
||||
12. 可配置presented content 阴影。
|
||||
|
||||
更多配置信息请参阅 [_HWPanModalPresentable.h_](https://github.com/HeathWang/HWPanModal/blob/master/Sources/Presentable/HWPanModalPresentable.h) 声明。
|
||||
|
||||
## 特别注意
|
||||
|
||||
1. 任何情况下,内部嵌套scrollable(UIScrollView,UITableView,UIWebView,UICollectionView),如果scrollable的contentSize变化了,务必调用`- (void)hw_panModalSetNeedsLayoutUpdate`刷新UI!!!
|
||||
2. 如果需要弹出浮层后push到下一层,使用`HWPanModalContentView`或者present vc用navigation 包一层。
|
||||
3. 请仔细阅读md,编译run示例代码,95%的功能在示例中都有展示,不要什么都不看就来问问题!!!
|
||||
|
||||
|
||||
### 支持UIViewController和继承自HWPanModalContentView弹出视图
|
||||
|
||||
从0.6.0版本后, 该框架支持使用 `HWPanModalContentView` 从底部弹出视图, 即实现了present ViewController同样的交互和动画。
|
||||
|
||||
不同点是 `HWPanModalContentView` 只是一个view视图, 通过添加一些动画实现了原本的功能。不像present ViewController的模式,你可以获得controller的整个生命周期,并且可以使用navigation栈来push VC。
|
||||
|
||||
`HWPanModalContentView` 目前的限制:
|
||||
* 不支持转屏。
|
||||
* 不支持屏幕边缘横向拖拽来dismiss。
|
||||
* 不支持自定义presenting VC动画。(因为是view,没有presenting VC)
|
||||
|
||||
|
||||
## 适配
|
||||
**iOS 8.0+**, support Objective-C & Swift.
|
||||
|
||||
## 安装
|
||||
|
||||
### [CocoaPods](https://guides.cocoapods.org/using/using-cocoapods.html)
|
||||
|
||||
```ruby
|
||||
pod 'HWPanModal', '~> 0.9.4'
|
||||
```
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 如何从底部弹出控制器
|
||||
只需要视图控制器适配 `HWPanModalPresentable` 协议即可. 默认情况下,不用重写适配的各个方法,如果需要自定义,请实现协议方法。
|
||||
|
||||
更多的自定义UI配置,请参见`HWPanModalPresentable`协议中每个方法的说明。
|
||||
|
||||
```Objective-C
|
||||
#import <HWPanModal/HWPanModal.h>
|
||||
@interface HWBaseViewController () <HWPanModalPresentable>
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWBaseViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentable
|
||||
- (PanModalHeight)longFormHeight {
|
||||
return PanModalHeightMake(PanModalHeightTypeMaxTopInset, 44);
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
弹出控制器:
|
||||
|
||||
```Objective-C
|
||||
#import <HWPanModal/HWPanModal.h>
|
||||
[self presentPanModal:[HWBaseViewController new]];
|
||||
```
|
||||
|
||||
就是这么简单。
|
||||
|
||||
### 如何主动更新控制器UI
|
||||
请查阅 `UIViewController+Presentation.h`,里面有详细说明。
|
||||
* Change the state between short and long form. call `- (void)hw_panModalTransitionTo:(PresentationState)state;`
|
||||
* Change ScrollView ContentOffset. call `- (void)hw_panModalSetContentOffset:(CGPoint)offset;`
|
||||
* Reload layout. call `- (void)hw_panModalSetNeedsLayoutUpdate;`
|
||||
* 注意:如果scrollable view的contentSize改变了,你必须调用改reload方法来更新UI。
|
||||
|
||||
### 自定义presenting VC动画编写
|
||||
|
||||
1. Create object conforms `HWPresentingViewControllerAnimatedTransitioning` .
|
||||
|
||||
```Objective-C
|
||||
|
||||
@interface HWMyCustomAnimation : NSObject <HWPresentingViewControllerAnimatedTransitioning>
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWMyCustomAnimation
|
||||
|
||||
|
||||
- (void)presentAnimateTransition:(id<HWPresentingViewControllerContextTransitioning>)transitionContext {
|
||||
NSTimeInterval duration = [transitionContext transitionDuration];
|
||||
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
// replace it.
|
||||
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
fromVC.view.transform = CGAffineTransformMakeScale(0.95, 0.95);
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAnimateTransition:(id<HWPresentingViewControllerContextTransitioning>)transitionContext {
|
||||
NSTimeInterval duration = [transitionContext transitionDuration];
|
||||
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
// replace it.
|
||||
[UIView animateWithDuration:duration animations:^{
|
||||
toVC.view.transform = CGAffineTransformIdentity;
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
1. Overwrite below two method.
|
||||
|
||||
```Objective-C
|
||||
- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle {
|
||||
return PresentingViewControllerAnimationStyleCustom;
|
||||
}
|
||||
|
||||
- (id<HWPresentingViewControllerAnimatedTransitioning>)customPresentingVCAnimation {
|
||||
return self.customAnimation;
|
||||
}
|
||||
|
||||
- (HWMyCustomAnimation *)customAnimation {
|
||||
if (!_customAnimation) {
|
||||
_customAnimation = [HWMyCustomAnimation new];
|
||||
}
|
||||
return _customAnimation;
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义指示器indicator view
|
||||
|
||||
You just need to create your own UIView, then adopt `HWPanModalIndicatorProtocol`.
|
||||
|
||||
In your presented controller, return it:
|
||||
|
||||
```Objective-C
|
||||
- (nullable UIView <HWPanModalIndicatorProtocol> *)customIndicatorView {
|
||||
HWTextIndicatorView *textIndicatorView = [HWTextIndicatorView new];
|
||||
return textIndicatorView;
|
||||
}
|
||||
```
|
||||
|
||||
Here is `HWTextIndicatorView` code:
|
||||
|
||||
```Objective-C
|
||||
@interface HWTextIndicatorView : UIView <HWPanModalIndicatorProtocol>
|
||||
|
||||
@end
|
||||
|
||||
@interface HWTextIndicatorView ()
|
||||
@property (nonatomic, strong) UILabel *stateLabel;
|
||||
@end
|
||||
|
||||
@implementation HWTextIndicatorView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
// init the _stateLabel
|
||||
[self addSubview:_stateLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (void)didChangeToState:(HWIndicatorState)state {
|
||||
switch (state) {
|
||||
case HWIndicatorStateNormal: {
|
||||
self.stateLabel.text = @"Please pull down to dismiss";
|
||||
self.stateLabel.textColor = [UIColor whiteColor];
|
||||
}
|
||||
break;
|
||||
case HWIndicatorStatePullDown: {
|
||||
self.stateLabel.text = @"Keep pull down to dismiss";
|
||||
self.stateLabel.textColor = [UIColor colorWithRed:1.000 green:0.200 blue:0.000 alpha:1.00];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)indicatorSize {
|
||||
return CGSizeMake(200, 18);
|
||||
}
|
||||
|
||||
- (void)setupSubviews {
|
||||
self.stateLabel.frame = self.bounds;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
### 如何使用HWPanModalContentView
|
||||
|
||||
你必须继承自 `HWPanModalContentView`. `HWPanModalContentView` 适配 `HWPanModalPresentable` 协议,就像你可用该协议来present一样。
|
||||
|
||||
```Objective-C
|
||||
@interface HWSimplePanModalView : HWPanModalContentView
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWSimplePanModalView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
// add view and layout.
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// present it.
|
||||
HWSimplePanModalView *simplePanModalView = [HWSimplePanModalView new];
|
||||
[simplePanModalView presentInView:nil];
|
||||
```
|
||||
|
||||
|
||||
## 例子
|
||||
|
||||
1. 克隆项目
|
||||
2. 然后执行 `pod install`
|
||||
3. 打开 HWPanModal.xcworkspace, 选择OC或者Swift项目运行
|
||||
|
||||
###### 我分别编写了纯`Objective-C` & `Swift`例子,基本涵盖了该framework的所有API使用。
|
||||
|
||||
## 联系我
|
||||
|
||||
Heath Wang
|
||||
yishu.jay@gmail.com
|
||||
|
||||
## WX
|
||||
|
||||
<p style="align: left">
|
||||
<a>
|
||||
<img src="images/groupChat.jpg" width="277">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## License
|
||||
|
||||
<b>HWPanModal</b> is released under a MIT License. See LICENSE file for details.
|
||||
|
||||
|
||||
27
Pods/HWPanModal/Sources/Animator/HWPanModalAnimator.h
generated
Normal file
27
Pods/HWPanModal/Sources/Animator/HWPanModalAnimator.h
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// HWPanModalAnimator.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void(^AnimationBlockType)(void);
|
||||
typedef void(^AnimationCompletionType)(BOOL completion);
|
||||
|
||||
static NSTimeInterval kTransitionDuration = 0.5;
|
||||
|
||||
@interface HWPanModalAnimator : NSObject
|
||||
|
||||
+ (void)animate:(AnimationBlockType)animations config:(nullable id <HWPanModalPresentable>)config completion:(nullable AnimationCompletionType)completion;
|
||||
|
||||
+ (void)dismissAnimate:(AnimationBlockType)animations config:(nullable id <HWPanModalPresentable>)config completion:(AnimationCompletionType)completion;
|
||||
|
||||
+ (void)smoothAnimate:(AnimationBlockType)animations duration:(NSTimeInterval)duration completion:(nullable AnimationCompletionType)completion;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
40
Pods/HWPanModal/Sources/Animator/HWPanModalAnimator.m
generated
Normal file
40
Pods/HWPanModal/Sources/Animator/HWPanModalAnimator.m
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// HWPanModalAnimator.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import "HWPanModalAnimator.h"
|
||||
|
||||
@implementation HWPanModalAnimator
|
||||
|
||||
+ (void)animate:(AnimationBlockType)animations config:(nullable id<HWPanModalPresentable>)config completion:(AnimationCompletionType)completion {
|
||||
[HWPanModalAnimator animate:animations config:config startingFromPercent:1 isPresentation:YES completion:completion];
|
||||
}
|
||||
|
||||
+ (void)dismissAnimate:(AnimationBlockType)animations config:(nullable id<HWPanModalPresentable>)config completion:(AnimationCompletionType)completion {
|
||||
[HWPanModalAnimator animate:animations config:config startingFromPercent:1 isPresentation:NO completion:completion];
|
||||
}
|
||||
|
||||
+ (void)animate:(AnimationBlockType)animations config:(nullable id <HWPanModalPresentable>)config startingFromPercent:(CGFloat)animationPercent isPresentation:(BOOL)flag completion:(AnimationCompletionType)completion {
|
||||
|
||||
NSTimeInterval duration;
|
||||
if (flag) {
|
||||
duration = config ? [config transitionDuration] : kTransitionDuration;
|
||||
} else {
|
||||
duration = config ? [config dismissalDuration] : kTransitionDuration;
|
||||
}
|
||||
|
||||
duration = duration * MAX(animationPercent, 0);
|
||||
CGFloat springDamping = config ? [config springDamping] : 1.0;
|
||||
UIViewAnimationOptions options = config ? [config transitionAnimationOptions] : UIViewAnimationOptionPreferredFramesPerSecondDefault;
|
||||
|
||||
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:springDamping initialSpringVelocity:0 options:options animations:animations completion:completion];
|
||||
}
|
||||
|
||||
+ (void)smoothAnimate:(AnimationBlockType)animations duration:(NSTimeInterval)duration completion:(nullable AnimationCompletionType)completion {
|
||||
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:animations completion:completion];
|
||||
}
|
||||
|
||||
@end
|
||||
17
Pods/HWPanModal/Sources/Animator/HWPanModalInteractiveAnimator.h
generated
Normal file
17
Pods/HWPanModal/Sources/Animator/HWPanModalInteractiveAnimator.h
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// HWPanModalInteractiveAnimator.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/14.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanModalInteractiveAnimator : UIPercentDrivenInteractiveTransition
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
16
Pods/HWPanModal/Sources/Animator/HWPanModalInteractiveAnimator.m
generated
Normal file
16
Pods/HWPanModal/Sources/Animator/HWPanModalInteractiveAnimator.m
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// HWPanModalInteractiveAnimator.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/14.
|
||||
//
|
||||
|
||||
#import "HWPanModalInteractiveAnimator.h"
|
||||
|
||||
@implementation HWPanModalInteractiveAnimator
|
||||
|
||||
- (CGFloat)completionSpeed {
|
||||
return 0.618;
|
||||
}
|
||||
|
||||
@end
|
||||
28
Pods/HWPanModal/Sources/Animator/HWPanModalPresentationAnimator.h
generated
Normal file
28
Pods/HWPanModal/Sources/Animator/HWPanModalPresentationAnimator.h
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// HWPanModalPresentationAnimator.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, TransitionStyle) {
|
||||
TransitionStylePresentation,
|
||||
TransitionStyleDismissal,
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanModalPresentationAnimator : NSObject <UIViewControllerAnimatedTransitioning>
|
||||
|
||||
- (instancetype)initWithTransitionStyle:(TransitionStyle)transitionStyle interactiveMode:(PanModalInteractiveMode)mode;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)new NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
314
Pods/HWPanModal/Sources/Animator/HWPanModalPresentationAnimator.m
generated
Normal file
314
Pods/HWPanModal/Sources/Animator/HWPanModalPresentationAnimator.m
generated
Normal file
@@ -0,0 +1,314 @@
|
||||
//
|
||||
// HWPanModalPresentationAnimator.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import "HWPanModalPresentationAnimator.h"
|
||||
#import "HWPanModalAnimator.h"
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
#import "HWPanContainerView.h"
|
||||
#import "UIView+HW_Frame.h"
|
||||
#import "HWPageSheetPresentingAnimation.h"
|
||||
#import "HWShoppingCartPresentingAnimation.h"
|
||||
|
||||
@interface HWPresentingVCTransitionContext : NSObject <HWPresentingViewControllerContextTransitioning>
|
||||
|
||||
@property (nonatomic, weak) UIViewController *fromVC;
|
||||
@property (nonatomic, weak) UIViewController *toVC;
|
||||
@property (nonatomic, assign) NSTimeInterval duration;
|
||||
@property (nonatomic, strong) UIView *containerView;
|
||||
|
||||
- (instancetype)initWithFromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC duration:(NSTimeInterval)duration containerView:(UIView *)containerView;
|
||||
|
||||
@end
|
||||
|
||||
@interface HWPanModalPresentationAnimator ()
|
||||
|
||||
@property (nonatomic, assign) TransitionStyle transitionStyle;
|
||||
|
||||
@property (nullable, nonatomic, strong) UISelectionFeedbackGenerator *feedbackGenerator API_AVAILABLE(ios(10.0));
|
||||
@property (nonatomic, strong) HWPresentingVCTransitionContext *presentingVCTransitionContext;
|
||||
@property (nonatomic, assign) PanModalInteractiveMode interactiveMode;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalPresentationAnimator
|
||||
|
||||
- (instancetype)initWithTransitionStyle:(TransitionStyle)transitionStyle interactiveMode:(PanModalInteractiveMode)mode {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_transitionStyle = transitionStyle;
|
||||
_interactiveMode = mode;
|
||||
if (transitionStyle == TransitionStylePresentation) {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
_feedbackGenerator = [UISelectionFeedbackGenerator new];
|
||||
[_feedbackGenerator prepare];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出controller动画
|
||||
*/
|
||||
- (void)animatePresentation:(id<UIViewControllerContextTransitioning>)context {
|
||||
|
||||
UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
if (!toVC && !fromVC)
|
||||
return;
|
||||
|
||||
UIViewController<HWPanModalPresentable> *presentable = [self panModalViewController:context];
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
// If you are implementing a custom container controller, use this method to tell the child that its views are about to appear or disappear.
|
||||
[fromVC beginAppearanceTransition:NO animated:YES];
|
||||
[self beginAppearanceTransitionForController:toVC isAppearing:YES animated:YES];
|
||||
}
|
||||
|
||||
|
||||
CGFloat yPos = presentable.shortFormYPos;
|
||||
if ([presentable originPresentationState] == PresentationStateLong) {
|
||||
yPos = presentable.longFormYPos;
|
||||
} else if ([presentable originPresentationState] == PresentationStateMedium) {
|
||||
yPos = presentable.mediumFormYPos;
|
||||
}
|
||||
|
||||
UIView *panView = context.containerView.panContainerView ?: toVC.view;
|
||||
panView.frame = [context finalFrameForViewController:toVC];
|
||||
panView.hw_top = context.containerView.frame.size.height;
|
||||
|
||||
if ([presentable isHapticFeedbackEnabled]) {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
[self.feedbackGenerator selectionChanged];
|
||||
}
|
||||
}
|
||||
|
||||
[HWPanModalAnimator animate:^{
|
||||
panView.hw_top = yPos;
|
||||
} config:presentable completion:^(BOOL completion) {
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
[fromVC endAppearanceTransition];
|
||||
[self endAppearanceTransitionForController:toVC];
|
||||
}
|
||||
|
||||
if (@available(iOS 10.0, *)) {
|
||||
self.feedbackGenerator = nil;
|
||||
}
|
||||
|
||||
[context completeTransition:completion];
|
||||
}];
|
||||
|
||||
self.presentingVCTransitionContext = [[HWPresentingVCTransitionContext alloc] initWithFromVC:fromVC toVC:toVC duration:[presentable transitionDuration] containerView:context.containerView];
|
||||
[self presentAnimationForPresentingVC:presentable];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使弹出controller消失动画
|
||||
*/
|
||||
- (void)animateDismissal:(id<UIViewControllerContextTransitioning>)context {
|
||||
|
||||
UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
if (!fromVC && !toVC)
|
||||
return;
|
||||
|
||||
UIViewController<HWPanModalPresentable> *presentable = [self panModalViewController:context];
|
||||
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
[self beginAppearanceTransitionForController:fromVC isAppearing:NO animated:YES];
|
||||
[toVC beginAppearanceTransition:YES animated:YES];
|
||||
}
|
||||
|
||||
UIView *panView = context.containerView.panContainerView ?: fromVC.view;
|
||||
self.presentingVCTransitionContext = [[HWPresentingVCTransitionContext alloc] initWithFromVC:fromVC toVC:toVC duration:[presentable transitionDuration] containerView:context.containerView];
|
||||
|
||||
// user toggle pan gesture to dismiss.
|
||||
if ([context isInteractive]) {
|
||||
[self interactionDismiss:context fromVC:fromVC toVC:toVC presentable:presentable panView:panView];
|
||||
} else {
|
||||
[self springDismiss:context fromVC:fromVC toVC:toVC presentable:presentable panView:panView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)springDismiss:(id <UIViewControllerContextTransitioning>)context fromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC presentable:(UIViewController <HWPanModalPresentable> *)presentable panView:(UIView *)panView {
|
||||
CGFloat offsetY = 0;
|
||||
HWPanModalShadow *shadowConfig = [presentable contentShadow];
|
||||
if (shadowConfig.shadowColor) {
|
||||
// we should make the panView move further to hide the shadow effect.
|
||||
offsetY = offsetY + shadowConfig.shadowRadius + shadowConfig.shadowOffset.height;
|
||||
if ([presentable showDragIndicator]) {
|
||||
offsetY += [presentable customIndicatorView] ? [presentable customIndicatorView].indicatorSize.height : 13;
|
||||
}
|
||||
}
|
||||
|
||||
[HWPanModalAnimator dismissAnimate:^{
|
||||
[self dismissAnimationForPresentingVC:presentable];
|
||||
panView.hw_top = (context.containerView.frame.size.height + offsetY);
|
||||
} config:presentable completion:^(BOOL completion) {
|
||||
[fromVC.view removeFromSuperview];
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
[self endAppearanceTransitionForController:fromVC];
|
||||
[toVC endAppearanceTransition];
|
||||
}
|
||||
|
||||
[context completeTransition:completion];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)interactionDismiss:(id <UIViewControllerContextTransitioning>)context fromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC presentable:(UIViewController <HWPanModalPresentable> *)presentable panView:(UIView *)panView {
|
||||
[HWPanModalAnimator smoothAnimate:^{
|
||||
if (self.interactiveMode == PanModalInteractiveModeSideslip) {
|
||||
panView.hw_left = panView.hw_width;
|
||||
}
|
||||
|
||||
[self dismissAnimationForPresentingVC:presentable];
|
||||
} duration:[presentable dismissalDuration] completion:^(BOOL completion) {
|
||||
// 因为会有手势交互,所以需要判断transitions是否cancel
|
||||
BOOL finished = ![context transitionWasCancelled];
|
||||
|
||||
if (finished) {
|
||||
[fromVC.view removeFromSuperview];
|
||||
|
||||
if ([presentable shouldEnableAppearanceTransition]) {
|
||||
[self endAppearanceTransitionForController:fromVC];
|
||||
[toVC endAppearanceTransition];
|
||||
}
|
||||
|
||||
context.containerView.userInteractionEnabled = YES;
|
||||
}
|
||||
[context completeTransition:finished];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - presenting VC animation
|
||||
|
||||
- (void)presentAnimationForPresentingVC:(UIViewController<HWPanModalPresentable> *)presentable {
|
||||
id<HWPresentingViewControllerAnimatedTransitioning> presentingAnimation = [self presentingVCAnimation:presentable];
|
||||
if (presentingAnimation) {
|
||||
[presentingAnimation presentAnimateTransition:self.presentingVCTransitionContext];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dismissAnimationForPresentingVC:(UIViewController<HWPanModalPresentable> *)presentable {
|
||||
id<HWPresentingViewControllerAnimatedTransitioning> presentingAnimation = [self presentingVCAnimation:presentable];
|
||||
if (presentingAnimation) {
|
||||
[presentingAnimation dismissAnimateTransition:self.presentingVCTransitionContext];
|
||||
}
|
||||
}
|
||||
|
||||
- (UIViewController <HWPanModalPresentable> *)panModalViewController:(id <UIViewControllerContextTransitioning>)context {
|
||||
switch (self.transitionStyle) {
|
||||
case TransitionStylePresentation: {
|
||||
UIViewController *controller = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
if ([controller conformsToProtocol:@protocol(HWPanModalPresentable)]) {
|
||||
return (UIViewController <HWPanModalPresentable> *) controller;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
case TransitionStyleDismissal: {
|
||||
UIViewController *controller = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
if ([controller conformsToProtocol:@protocol(HWPanModalPresentable)]) {
|
||||
return (UIViewController <HWPanModalPresentable> *) controller;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIViewControllerAnimatedTransitioning
|
||||
|
||||
- (void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
|
||||
switch (self.transitionStyle) {
|
||||
case TransitionStylePresentation: {
|
||||
[self animatePresentation:transitionContext];
|
||||
}
|
||||
break;
|
||||
case TransitionStyleDismissal: {
|
||||
[self animateDismissal:transitionContext];
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
|
||||
if (transitionContext && [self panModalViewController:transitionContext]) {
|
||||
UIViewController<HWPanModalPresentable> *controller = [self panModalViewController:transitionContext];
|
||||
return [controller transitionDuration];
|
||||
}
|
||||
return kTransitionDuration;
|
||||
}
|
||||
|
||||
#pragma mark - presenting animated transition
|
||||
|
||||
- (id<HWPresentingViewControllerAnimatedTransitioning>)presentingVCAnimation:(UIViewController<HWPanModalPresentable> *)presentable {
|
||||
switch ([presentable presentingVCAnimationStyle]) {
|
||||
case PresentingViewControllerAnimationStylePageSheet:
|
||||
return [HWPageSheetPresentingAnimation new];
|
||||
case PresentingViewControllerAnimationStyleShoppingCart:
|
||||
return [HWShoppingCartPresentingAnimation new];
|
||||
case PresentingViewControllerAnimationStyleCustom:
|
||||
return [presentable customPresentingVCAnimation];
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (void)beginAppearanceTransitionForController:(UIViewController *)viewController isAppearing:(BOOL)isAppearing animated:(BOOL)animated {
|
||||
// Fix `The unbalanced calls to begin/end appearance transitions` warning.
|
||||
if (![viewController isKindOfClass:UINavigationController.class]) {
|
||||
[viewController beginAppearanceTransition:isAppearing animated:animated];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)endAppearanceTransitionForController:(UIViewController *)viewController {
|
||||
if (![viewController isKindOfClass:UINavigationController.class]) {
|
||||
[viewController endAppearanceTransition];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPresentingVCTransitionContext
|
||||
|
||||
- (instancetype)initWithFromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC duration:(NSTimeInterval)duration containerView:(UIView *)containerView {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_fromVC = fromVC;
|
||||
_toVC = toVC;
|
||||
_duration = duration;
|
||||
_containerView = containerView;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (__kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key {
|
||||
if ([key isEqualToString:UITransitionContextFromViewControllerKey]) {
|
||||
return self.fromVC;
|
||||
} else if ([key isEqualToString:UITransitionContextToViewControllerKey]) {
|
||||
return self.toVC;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)transitionDuration {
|
||||
return self.duration;
|
||||
}
|
||||
|
||||
@end
|
||||
51
Pods/HWPanModal/Sources/Animator/HWPresentingVCAnimatedTransitioning.h
generated
Normal file
51
Pods/HWPanModal/Sources/Animator/HWPresentingVCAnimatedTransitioning.h
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// HWCustomPresentingVCAnimatedTransitioning.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/6/12.
|
||||
//
|
||||
|
||||
#ifndef HWCustomPresentingVCAnimatedTransitioning_h
|
||||
#define HWCustomPresentingVCAnimatedTransitioning_h
|
||||
|
||||
NS_SWIFT_NAME(PanModalPresentingViewControllerContextTransitioning)
|
||||
@protocol HWPresentingViewControllerContextTransitioning <NSObject>
|
||||
|
||||
/**
|
||||
* Returns a view controller involved in the transition.
|
||||
* @return The view controller object for the specified key or nil if the view controller could not be found.
|
||||
*/
|
||||
- (__kindof UIViewController * _Nullable )viewControllerForKey:(nonnull UITransitionContextViewControllerKey)key;
|
||||
|
||||
/**
|
||||
* The Animation duration gets from ViewController which conforms HWPanModalPresentable
|
||||
* - (NSTimeInterval)transitionDuration;
|
||||
*/
|
||||
- (NSTimeInterval)transitionDuration;
|
||||
|
||||
/**
|
||||
* Transition container, from UIViewControllerContextTransitioning protocol
|
||||
*/
|
||||
@property(nonnull, nonatomic, readonly) UIView *containerView;
|
||||
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(PanModalPresentingViewControllerAnimatedTransitioning)
|
||||
@protocol HWPresentingViewControllerAnimatedTransitioning <NSObject>
|
||||
|
||||
/**
|
||||
* Write you custom animation when present.
|
||||
*/
|
||||
- (void)presentAnimateTransition:(nonnull id<HWPresentingViewControllerContextTransitioning>)context NS_SWIFT_NAME(presentTransition(context:));
|
||||
/**
|
||||
* Write you custom animation when dismiss.
|
||||
*/
|
||||
- (void)dismissAnimateTransition:(nonnull id<HWPresentingViewControllerContextTransitioning>)context NS_SWIFT_NAME(dismissTransition(context:));
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#endif /* HWCustomPresentingVCAnimatedTransitioning_h */
|
||||
|
||||
|
||||
|
||||
17
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWPageSheetPresentingAnimation.h
generated
Normal file
17
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWPageSheetPresentingAnimation.h
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// HWPageSheetPresentingAnimation.h
|
||||
// HWPanModal-iOS10.0
|
||||
//
|
||||
// Created by heath wang on 2019/9/5.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HWPresentingVCAnimatedTransitioning.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPageSheetPresentingAnimation : NSObject <HWPresentingViewControllerAnimatedTransitioning>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
29
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWPageSheetPresentingAnimation.m
generated
Normal file
29
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWPageSheetPresentingAnimation.m
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// HWPageSheetPresentingAnimation.m
|
||||
// HWPanModal-iOS10.0
|
||||
//
|
||||
// Created by heath wang on 2019/9/5.
|
||||
//
|
||||
|
||||
#import "HWPageSheetPresentingAnimation.h"
|
||||
|
||||
@implementation HWPageSheetPresentingAnimation
|
||||
|
||||
- (void)presentAnimateTransition:(nonnull id <HWPresentingViewControllerContextTransitioning>)context {
|
||||
NSTimeInterval duration = [context transitionDuration];
|
||||
UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
[UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.9 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
|
||||
CGFloat scale = 1 - statusBarHeight * 2 / CGRectGetHeight(fromVC.view.bounds);
|
||||
fromVC.view.transform = CGAffineTransformMakeScale(scale, scale);
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAnimateTransition:(nonnull id <HWPresentingViewControllerContextTransitioning>)context {
|
||||
UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
toVC.view.transform = CGAffineTransformIdentity;
|
||||
}
|
||||
|
||||
@end
|
||||
17
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWShoppingCartPresentingAnimation.h
generated
Normal file
17
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWShoppingCartPresentingAnimation.h
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// HWShoppingCartPresentingAnimation.h
|
||||
// HWPanModal-iOS10.0
|
||||
//
|
||||
// Created by heath wang on 2019/9/5.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HWPresentingVCAnimatedTransitioning.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWShoppingCartPresentingAnimation : NSObject <HWPresentingViewControllerAnimatedTransitioning>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
39
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWShoppingCartPresentingAnimation.m
generated
Normal file
39
Pods/HWPanModal/Sources/Animator/PresentingVCAnimation/HWShoppingCartPresentingAnimation.m
generated
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// HWShoppingCartPresentingAnimation.m
|
||||
// HWPanModal-iOS10.0
|
||||
//
|
||||
// Created by heath wang on 2019/9/5.
|
||||
//
|
||||
|
||||
#import "HWShoppingCartPresentingAnimation.h"
|
||||
|
||||
@implementation HWShoppingCartPresentingAnimation
|
||||
|
||||
- (void)presentAnimateTransition:(nonnull id <HWPresentingViewControllerContextTransitioning>)context {
|
||||
|
||||
NSTimeInterval duration = [context transitionDuration];
|
||||
UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
|
||||
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
|
||||
CGFloat scale = 1 - statusBarHeight * 2 / CGRectGetHeight(fromVC.view.bounds);
|
||||
[UIView animateWithDuration:duration * 0.4 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
|
||||
CATransform3D tran = CATransform3DIdentity;
|
||||
tran.m34 = -1 / 1000.0f;
|
||||
tran = CATransform3DRotate(tran, M_PI / 16, 1, 0, 0);
|
||||
tran = CATransform3DTranslate(tran, 0, 0, -100);
|
||||
fromVC.view.layer.transform = tran;
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
[UIView animateWithDuration:duration * 0.6 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
|
||||
fromVC.view.layer.transform = CATransform3DMakeScale(scale, scale, 1);
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAnimateTransition:(nonnull id <HWPresentingViewControllerContextTransitioning>)context {
|
||||
UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];
|
||||
toVC.view.layer.transform = CATransform3DIdentity;
|
||||
}
|
||||
|
||||
@end
|
||||
18
Pods/HWPanModal/Sources/Category/UIScrollView+Helper.h
generated
Normal file
18
Pods/HWPanModal/Sources/Category/UIScrollView+Helper.h
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// UIScrollView+Helper.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/15.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIScrollView (Helper)
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL isScrolling;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
16
Pods/HWPanModal/Sources/Category/UIScrollView+Helper.m
generated
Normal file
16
Pods/HWPanModal/Sources/Category/UIScrollView+Helper.m
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// UIScrollView+Helper.m
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/15.
|
||||
//
|
||||
|
||||
#import "UIScrollView+Helper.h"
|
||||
|
||||
@implementation UIScrollView (Helper)
|
||||
|
||||
- (BOOL)isScrolling {
|
||||
return (self.isDragging && !self.isDecelerating) || self.isTracking;
|
||||
}
|
||||
|
||||
@end
|
||||
28
Pods/HWPanModal/Sources/Category/UIView+HW_Frame.h
generated
Normal file
28
Pods/HWPanModal/Sources/Category/UIView+HW_Frame.h
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// UIView+HW_Frame.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/20.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIView (HW_Frame)
|
||||
|
||||
@property (nonatomic, assign) CGFloat hw_left; ///< Shortcut for frame.origin.x.
|
||||
@property (nonatomic, assign) CGFloat hw_top; ///< Shortcut for frame.origin.y
|
||||
@property (nonatomic, assign) CGFloat hw_right; ///< Shortcut for frame.origin.x + frame.size.width
|
||||
@property (nonatomic, assign) CGFloat hw_bottom; ///< Shortcut for frame.origin.y + frame.size.height
|
||||
@property (nonatomic, assign) CGFloat hw_width; ///< Shortcut for frame.size.width.
|
||||
@property (nonatomic, assign) CGFloat hw_height; ///< Shortcut for frame.size.height.
|
||||
@property (nonatomic, assign) CGFloat hw_centerX; ///< Shortcut for center.x
|
||||
@property (nonatomic, assign) CGFloat hw_centerY; ///< Shortcut for center.y
|
||||
@property (nonatomic, assign) CGPoint hw_origin; ///< Shortcut for frame.origin.
|
||||
@property (nonatomic, assign) CGSize hw_size; ///< Shortcut for frame.size.
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
109
Pods/HWPanModal/Sources/Category/UIView+HW_Frame.m
generated
Normal file
109
Pods/HWPanModal/Sources/Category/UIView+HW_Frame.m
generated
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// UIView+HW_Frame.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/20.
|
||||
//
|
||||
|
||||
#import "UIView+HW_Frame.h"
|
||||
|
||||
@implementation UIView (HW_Frame)
|
||||
|
||||
- (CGFloat)hw_left {
|
||||
return self.frame.origin.x;
|
||||
}
|
||||
|
||||
- (void)setHw_left:(CGFloat)hwLeft {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin.x = hwLeft;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_top {
|
||||
return self.frame.origin.y;
|
||||
}
|
||||
|
||||
- (void)setHw_top:(CGFloat)hwTop {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin.y = hwTop;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_right {
|
||||
return self.frame.origin.x + self.frame.size.width;
|
||||
}
|
||||
|
||||
- (void)setHw_right:(CGFloat)hwRight {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin.x = hwRight - self.frame.size.width;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_bottom {
|
||||
return self.frame.origin.y + self.frame.size.height;
|
||||
}
|
||||
|
||||
- (void)setHw_bottom:(CGFloat)hwBottom {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin.y = hwBottom - self.frame.size.height;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_width {
|
||||
return self.frame.size.width;
|
||||
}
|
||||
|
||||
- (void)setHw_width:(CGFloat)hwWidth {
|
||||
CGRect frame = self.frame;
|
||||
frame.size.width = hwWidth;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_height {
|
||||
return self.frame.size.height;
|
||||
}
|
||||
|
||||
- (void)setHw_height:(CGFloat)hwHeight {
|
||||
CGRect frame = self.frame;
|
||||
frame.size.height = hwHeight;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGFloat)hw_centerX {
|
||||
return self.center.x;
|
||||
}
|
||||
|
||||
- (void)setHw_centerX:(CGFloat)hwCenterX {
|
||||
self.center = CGPointMake(hwCenterX, self.center.y);
|
||||
}
|
||||
|
||||
- (CGFloat)hw_centerY {
|
||||
return self.center.y;
|
||||
}
|
||||
|
||||
- (void)setHw_centerY:(CGFloat)hwCenterY {
|
||||
self.center = CGPointMake(self.center.x, hwCenterY);
|
||||
}
|
||||
|
||||
- (CGPoint)hw_origin {
|
||||
return self.frame.origin;
|
||||
}
|
||||
|
||||
- (void)setHw_origin:(CGPoint)hwOrigin {
|
||||
CGRect frame = self.frame;
|
||||
frame.origin = hwOrigin;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (CGSize)hw_size {
|
||||
return self.frame.size;
|
||||
}
|
||||
|
||||
- (void)setHw_size:(CGSize)hwSize {
|
||||
CGRect frame = self.frame;
|
||||
frame.size = hwSize;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
32
Pods/HWPanModal/Sources/Controller/HWPanModalPresentationController.h
generated
Normal file
32
Pods/HWPanModal/Sources/Controller/HWPanModalPresentationController.h
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// HWPanModalPresentationController.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
@class HWDimmedView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
@interface HWPanModalPresentationController : UIPresentationController
|
||||
|
||||
@property (nonatomic, readonly) HWDimmedView *backgroundView;
|
||||
@property (nonatomic, readonly) PresentationState currentPresentationState;
|
||||
|
||||
- (void)setNeedsLayoutUpdate;
|
||||
|
||||
- (void)updateUserHitBehavior;
|
||||
|
||||
- (void)transitionToState:(PresentationState)state animated:(BOOL)animated;
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated;
|
||||
|
||||
- (void)dismissAnimated:(BOOL)animated completion:(void (^)(void))completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
599
Pods/HWPanModal/Sources/Controller/HWPanModalPresentationController.m
generated
Normal file
599
Pods/HWPanModal/Sources/Controller/HWPanModalPresentationController.m
generated
Normal file
@@ -0,0 +1,599 @@
|
||||
//
|
||||
// HWPanModalPresentationController.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import "HWPanModalPresentationController.h"
|
||||
#import "HWDimmedView.h"
|
||||
#import "HWPanContainerView.h"
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
#import "HWPanModalAnimator.h"
|
||||
#import "HWPanModalInteractiveAnimator.h"
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
#import "UIViewController+PanModalPresenter.h"
|
||||
#import "HWPanIndicatorView.h"
|
||||
#import "UIView+HW_Frame.h"
|
||||
#import "HWPanModalPresentableHandler.h"
|
||||
|
||||
@interface HWPanModalPresentationController () <UIGestureRecognizerDelegate, HWPanModalPresentableHandlerDelegate, HWPanModalPresentableHandlerDataSource>
|
||||
|
||||
// 判断弹出的view是否在做动画
|
||||
@property (nonatomic, assign) BOOL isPresentedViewAnimating;
|
||||
@property (nonatomic, assign) PresentationState currentPresentationState;
|
||||
|
||||
@property (nonatomic, strong) id<HWPanModalPresentable> presentable;
|
||||
|
||||
// view
|
||||
@property (nonatomic, strong) HWDimmedView *backgroundView;
|
||||
@property (nonatomic, strong) HWPanContainerView *panContainerView;
|
||||
@property (nonatomic, strong) UIView<HWPanModalIndicatorProtocol> *dragIndicatorView;
|
||||
|
||||
@property (nonatomic, strong) HWPanModalPresentableHandler *handler;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalPresentationController
|
||||
|
||||
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController {
|
||||
self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];
|
||||
if (self) {
|
||||
_handler = [[HWPanModalPresentableHandler alloc] initWithPresentable:[self presentable]];
|
||||
_handler.delegate = self;
|
||||
_handler.dataSource = self;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - overridden
|
||||
|
||||
- (UIView *)presentedView {
|
||||
return self.panContainerView;
|
||||
}
|
||||
|
||||
- (void)containerViewWillLayoutSubviews {
|
||||
[super containerViewWillLayoutSubviews];
|
||||
[self configureViewLayout];
|
||||
}
|
||||
|
||||
#pragma mark - Tracking the Transition Start and End
|
||||
|
||||
- (void)presentationTransitionWillBegin {
|
||||
[[self presentable] panModalTransitionWillBegin];
|
||||
|
||||
if (!self.containerView)
|
||||
return;
|
||||
|
||||
[self layoutBackgroundView:self.containerView];
|
||||
|
||||
if ([[self presentable] originPresentationState] == PresentationStateLong) {
|
||||
self.currentPresentationState = PresentationStateLong;
|
||||
} else if ([[self presentable] originPresentationState] == PresentationStateMedium) {
|
||||
self.currentPresentationState = PresentationStateMedium;
|
||||
}
|
||||
|
||||
[self layoutPresentedView:self.containerView];
|
||||
[self.handler configureScrollViewInsets];
|
||||
|
||||
if (!self.presentedViewController.transitionCoordinator) {
|
||||
self.backgroundView.dimState = DimStateMax;
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) wkSelf = self;
|
||||
__block BOOL isAnimated = NO;
|
||||
[self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
wkSelf.backgroundView.dimState = DimStateMax;
|
||||
[wkSelf.presentedViewController setNeedsStatusBarAppearanceUpdate];
|
||||
isAnimated = YES;
|
||||
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||
if (!isAnimated) {
|
||||
/// In some cases, for example, present a `hw` when a navigation controller is pushing a new vc, `animateAlongsideTransition` will not call.
|
||||
/// If not called, call it here.
|
||||
wkSelf.backgroundView.dimState = DimStateMax;
|
||||
[wkSelf.presentedViewController setNeedsStatusBarAppearanceUpdate];
|
||||
}
|
||||
if ([[wkSelf presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
// hack TransitionView
|
||||
[wkSelf.containerView setValue:@(YES) forKey:@"ignoreDirectTouchEvents"];
|
||||
}
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
- (void)presentationTransitionDidEnd:(BOOL)completed {
|
||||
[[self presentable] panModalTransitionDidFinish];
|
||||
if (completed)
|
||||
return;
|
||||
|
||||
[self.backgroundView removeFromSuperview];
|
||||
[self.presentedView endEditing:YES];
|
||||
}
|
||||
|
||||
- (void)dismissalTransitionWillBegin {
|
||||
id <UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentedViewController.transitionCoordinator;
|
||||
if (!transitionCoordinator) {
|
||||
self.backgroundView.dimState = DimStateOff;
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) wkSelf = self;
|
||||
[transitionCoordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
wkSelf.dragIndicatorView.alpha = 0;
|
||||
wkSelf.backgroundView.dimState = DimStateOff;
|
||||
[wkSelf.presentedViewController setNeedsStatusBarAppearanceUpdate];
|
||||
} completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissalTransitionDidEnd:(BOOL)completed {
|
||||
if (completed) {
|
||||
// break the delegate
|
||||
self.delegate = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIContentContainer protocol
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||
|
||||
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
if (self && [self presentable]) {
|
||||
[self adjustPresentedViewFrame];
|
||||
|
||||
if ([self.presentable shouldRoundTopCorners]) {
|
||||
[self addRoundedCornersToView:self.panContainerView.contentView];
|
||||
}
|
||||
[self updateDragIndicatorView];
|
||||
}
|
||||
} completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
[self transitionToState:self.currentPresentationState animated:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - public method
|
||||
|
||||
- (void)setNeedsLayoutUpdate {
|
||||
[self configureViewLayout];
|
||||
[self adjustPresentedViewFrame];
|
||||
|
||||
[self updateBackgroundColor];
|
||||
[self updateContainerViewShadow];
|
||||
[self updateDragIndicatorView];
|
||||
[self updateRoundedCorners];
|
||||
|
||||
[self.handler observeScrollable];
|
||||
[self.handler configureScrollViewInsets];
|
||||
[self checkEdgeInteractive];
|
||||
}
|
||||
|
||||
- (void)transitionToState:(PresentationState)state animated:(BOOL)animated {
|
||||
|
||||
if (![self.presentable shouldTransitionToState:state])
|
||||
return;
|
||||
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
[self.presentable willTransitionToState:state];
|
||||
|
||||
switch (state) {
|
||||
case PresentationStateLong: {
|
||||
[self snapToYPos:self.handler.longFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
case PresentationStateMedium: {
|
||||
[self snapToYPos:self.handler.mediumFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
case PresentationStateShort: {
|
||||
[self snapToYPos:self.handler.shortFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
}
|
||||
self.currentPresentationState = state;
|
||||
[[self presentable] didChangeTransitionToState:state];
|
||||
}
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated {
|
||||
[self.handler setScrollableContentOffset:offset animated:animated];
|
||||
}
|
||||
|
||||
- (void)updateUserHitBehavior {
|
||||
[self checkVCContainerEventPass];
|
||||
[self checkBackgroundViewEventPass];
|
||||
}
|
||||
|
||||
#pragma mark - layout
|
||||
|
||||
- (void)adjustPresentedViewFrame {
|
||||
|
||||
if (!self.containerView)
|
||||
return;
|
||||
|
||||
CGRect frame = self.containerView.frame;
|
||||
CGSize size = CGSizeMake(CGRectGetWidth(frame), CGRectGetHeight(frame) - self.handler.anchoredYPosition);
|
||||
|
||||
self.presentedView.hw_size = frame.size;
|
||||
self.panContainerView.contentView.frame = CGRectMake(0, 0, size.width, size.height);
|
||||
self.presentedViewController.view.frame = self.panContainerView.contentView.bounds;
|
||||
[self.presentedViewController.view setNeedsLayout];
|
||||
[self.presentedViewController.view layoutIfNeeded];
|
||||
}
|
||||
|
||||
/**
|
||||
* add backGroundView并设置约束
|
||||
*/
|
||||
- (void)layoutBackgroundView:(UIView *)containerView {
|
||||
[containerView addSubview:self.backgroundView];
|
||||
[self updateBackgroundColor];
|
||||
self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
NSArray *hCons = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
|
||||
NSArray *vCons = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
|
||||
[NSLayoutConstraint activateConstraints:hCons];
|
||||
[NSLayoutConstraint activateConstraints:vCons];
|
||||
}
|
||||
|
||||
- (void)updateBackgroundColor {
|
||||
self.backgroundView.blurTintColor = [self.presentable backgroundConfig].blurTintColor;
|
||||
}
|
||||
|
||||
- (void)layoutPresentedView:(UIView *)containerView {
|
||||
if (!self.presentable)
|
||||
return;
|
||||
|
||||
self.handler.presentedView = self.presentedView;
|
||||
|
||||
[containerView addSubview:self.presentedView];
|
||||
[containerView addGestureRecognizer:self.handler.panGestureRecognizer];
|
||||
|
||||
if ([self.presentable allowScreenEdgeInteractive]) {
|
||||
[containerView addGestureRecognizer:self.handler.screenEdgeGestureRecognizer];
|
||||
[self.handler.screenEdgeGestureRecognizer addTarget:self action:@selector(screenEdgeInteractiveAction:)];
|
||||
}
|
||||
|
||||
[self setNeedsLayoutUpdate];
|
||||
[self adjustPanContainerBackgroundColor];
|
||||
|
||||
[[self presentable] presentedViewDidMoveToSuperView];
|
||||
}
|
||||
|
||||
- (void)adjustPanContainerBackgroundColor {
|
||||
self.panContainerView.contentView.backgroundColor = self.presentedViewController.view.backgroundColor ? : [self.presentable panScrollable].backgroundColor;
|
||||
}
|
||||
|
||||
- (void)updateDragIndicatorView {
|
||||
if ([self.presentable showDragIndicator]) {
|
||||
[self addDragIndicatorViewToView:self.panContainerView];
|
||||
} else {
|
||||
self.dragIndicatorView.hidden = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addDragIndicatorViewToView:(UIView *)view {
|
||||
// if has been add, won't update it.
|
||||
self.dragIndicatorView.hidden = NO;
|
||||
|
||||
CGSize indicatorSize = [self.dragIndicatorView indicatorSize];
|
||||
|
||||
if (self.dragIndicatorView.superview == view) {
|
||||
self.dragIndicatorView.frame = CGRectMake((view.hw_width - indicatorSize.width) / 2, -kIndicatorYOffset - indicatorSize.height, indicatorSize.width, indicatorSize.height);
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
return;
|
||||
}
|
||||
|
||||
self.handler.dragIndicatorView = self.dragIndicatorView;
|
||||
[view addSubview:self.dragIndicatorView];
|
||||
|
||||
self.dragIndicatorView.frame = CGRectMake((view.hw_width - indicatorSize.width) / 2, -kIndicatorYOffset - indicatorSize.height, indicatorSize.width, indicatorSize.height);
|
||||
[self.dragIndicatorView setupSubviews];
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
}
|
||||
|
||||
- (void)updateRoundedCorners {
|
||||
if ([self.presentable shouldRoundTopCorners]) {
|
||||
[self addRoundedCornersToView:self.panContainerView.contentView];
|
||||
} else {
|
||||
[self resetRoundedCornersToView:self.panContainerView.contentView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addRoundedCornersToView:(UIView *)view {
|
||||
CGFloat radius = [self.presentable cornerRadius];
|
||||
|
||||
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerTopLeft cornerRadii:CGSizeMake(radius, radius)];
|
||||
|
||||
CAShapeLayer *mask = [CAShapeLayer new];
|
||||
mask.path = bezierPath.CGPath;
|
||||
view.layer.mask = mask;
|
||||
|
||||
// 提高性能
|
||||
view.layer.shouldRasterize = YES;
|
||||
view.layer.rasterizationScale = [UIScreen mainScreen].scale;
|
||||
}
|
||||
|
||||
- (void)resetRoundedCornersToView:(UIView *)view {
|
||||
view.layer.mask = nil;
|
||||
view.layer.shouldRasterize = NO;
|
||||
}
|
||||
|
||||
- (void)updateContainerViewShadow {
|
||||
HWPanModalShadow *shadow = [[self presentable] contentShadow];
|
||||
if (shadow.shadowColor) {
|
||||
[self.panContainerView updateShadow:shadow.shadowColor shadowRadius:shadow.shadowRadius shadowOffset:shadow.shadowOffset shadowOpacity:shadow.shadowOpacity];
|
||||
} else {
|
||||
[self.panContainerView clearShadow];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates & stores the layout anchor points & options
|
||||
*/
|
||||
- (void)configureViewLayout {
|
||||
|
||||
[self.handler configureViewLayout];
|
||||
self.containerView.userInteractionEnabled = [[self presentable] isUserInteractionEnabled];
|
||||
}
|
||||
|
||||
#pragma mark - event passing through
|
||||
|
||||
- (void)checkVCContainerEventPass {
|
||||
BOOL eventPassValue = [[self presentable] allowsTouchEventsPassingThroughTransitionView];
|
||||
// hack TransitionView
|
||||
[self.containerView setValue:@(eventPassValue) forKey:@"ignoreDirectTouchEvents"];
|
||||
}
|
||||
|
||||
- (void)checkBackgroundViewEventPass {
|
||||
if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
self.backgroundView.userInteractionEnabled = NO;
|
||||
self.backgroundView.tapBlock = nil;
|
||||
} else {
|
||||
self.backgroundView.userInteractionEnabled = YES;
|
||||
__weak typeof(self) wkSelf = self;
|
||||
self.backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
|
||||
if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
|
||||
[wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - y position update
|
||||
|
||||
- (void)snapToYPos:(CGFloat)yPos animated:(BOOL)animated {
|
||||
|
||||
if (animated) {
|
||||
[HWPanModalAnimator animate:^{
|
||||
self.isPresentedViewAnimating = YES;
|
||||
[self adjustToYPos:yPos];
|
||||
} config:self.presentable completion:^(BOOL completion) {
|
||||
self.isPresentedViewAnimating = NO;
|
||||
}];
|
||||
} else {
|
||||
[self adjustToYPos:yPos];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)adjustToYPos:(CGFloat)yPos {
|
||||
self.presentedView.hw_top = MAX(yPos, self.handler.anchoredYPosition);
|
||||
|
||||
// change dim background starting from shortFormYPosition.
|
||||
if (self.presentedView.frame.origin.y >= self.handler.shortFormYPosition) {
|
||||
|
||||
CGFloat yDistanceFromShortForm = self.presentedView.frame.origin.y - self.handler.shortFormYPosition;
|
||||
CGFloat bottomHeight = self.containerView.hw_height - self.handler.shortFormYPosition;
|
||||
CGFloat percent = yDistanceFromShortForm / bottomHeight;
|
||||
self.backgroundView.dimState = DimStatePercent;
|
||||
self.backgroundView.percent = 1 - percent;
|
||||
|
||||
[self.presentable panModalGestureRecognizer:self.handler.panGestureRecognizer dismissPercent:MIN(percent, 1)];
|
||||
if (self.presentedViewController.isBeingDismissed) {
|
||||
[[self interactiveAnimator] updateInteractiveTransition:MIN(percent, 1)];
|
||||
}
|
||||
} else {
|
||||
self.backgroundView.dimState = DimStateMax;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentableHandlerDelegate
|
||||
|
||||
- (void)adjustPresentableYPos:(CGFloat)yPos {
|
||||
[self adjustToYPos:yPos];
|
||||
}
|
||||
|
||||
- (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode {
|
||||
[self dismiss:isInteractive mode:mode animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode animated:(BOOL)animated completion:(void (^)(void))completion {
|
||||
self.presentedViewController.hw_panModalPresentationDelegate.interactive = isInteractive;
|
||||
self.presentedViewController.hw_panModalPresentationDelegate.interactiveMode = mode;
|
||||
[self.presentable panModalWillDismiss];
|
||||
[self.presentedViewController dismissViewControllerAnimated:animated completion:^{
|
||||
if (completion) completion();
|
||||
[self.presentable panModalDidDismissed];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(BOOL)animated completion:(nonnull void (^)(void))completion {
|
||||
[self dismiss:NO mode:PanModalInteractiveModeNone animated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (void)presentableTransitionToState:(PresentationState)state {
|
||||
[self transitionToState:state animated:YES];
|
||||
}
|
||||
|
||||
- (PresentationState)getCurrentPresentationState {
|
||||
return self.currentPresentationState;
|
||||
}
|
||||
|
||||
#pragma mark - interactive handle
|
||||
|
||||
- (void)finishInteractiveTransition {
|
||||
if (self.presentedViewController.isBeingDismissed) {
|
||||
// make the containerView can not response event action.
|
||||
self.containerView.userInteractionEnabled = NO;
|
||||
[[self interactiveAnimator] finishInteractiveTransition];
|
||||
|
||||
if (self.presentedViewController.hw_panModalPresentationDelegate.interactiveMode != PanModalInteractiveModeDragDown)
|
||||
return;
|
||||
|
||||
if ([[self presentable] presentingVCAnimationStyle] > PresentingViewControllerAnimationStyleNone) {
|
||||
[HWPanModalAnimator animate:^{
|
||||
[self presentedView].hw_top = self.containerView.frame.size.height;
|
||||
self.dragIndicatorView.alpha = 0;
|
||||
self.backgroundView.dimState = DimStateOff;
|
||||
} config:[self presentable] completion:^(BOOL completion) {
|
||||
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelInteractiveTransition {
|
||||
if (self.presentedViewController.isBeingDismissed) {
|
||||
[[self interactiveAnimator] cancelInteractiveTransition];
|
||||
self.presentedViewController.hw_panModalPresentationDelegate.interactiveMode = PanModalInteractiveModeNone;
|
||||
self.presentedViewController.hw_panModalPresentationDelegate.interactive = NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentableHandlerDataSource
|
||||
|
||||
- (CGSize)containerSize {
|
||||
return self.containerView.bounds.size;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingDismissed {
|
||||
return self.presentedViewController.isBeingDismissed;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingPresented {
|
||||
return self.presentedViewController.isBeingPresented;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedViewAnchored {
|
||||
|
||||
if (![[self presentable] shouldRespondToPanModalGestureRecognizer:self.handler.panGestureRecognizer]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (!self.isPresentedViewAnimating && self.handler.extendsPanScrolling && (CGRectGetMinY(self.presentedView.frame) <= self.handler.anchoredYPosition || HW_TWO_FLOAT_IS_EQUAL(CGRectGetMinY(self.presentedView.frame), self.handler.anchoredYPosition))) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedControllerInteractive {
|
||||
return self.presentedViewController.hw_panModalPresentationDelegate.interactive;
|
||||
}
|
||||
|
||||
- (BOOL)isFormPositionAnimating {
|
||||
return self.isPresentedViewAnimating;
|
||||
}
|
||||
|
||||
#pragma mark - Screen Gesture enevt
|
||||
|
||||
- (void)screenEdgeInteractiveAction:(UIPanGestureRecognizer *)recognizer {
|
||||
CGPoint translation = [recognizer translationInView:recognizer.view];
|
||||
CGFloat percent = translation.x / CGRectGetWidth(recognizer.view.bounds);
|
||||
CGPoint velocity = [recognizer velocityInView:recognizer.view];
|
||||
|
||||
switch (recognizer.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
[self dismiss:YES mode:PanModalInteractiveModeSideslip];
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateEnded: {
|
||||
if (percent > 0.5 || velocity.x >= [[self presentable] minHorizontalVelocityToTriggerScreenEdgeDismiss]) {
|
||||
[self finishInteractiveTransition];
|
||||
} else {
|
||||
[self cancelInteractiveTransition];
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateChanged: {
|
||||
|
||||
[[self interactiveAnimator] updateInteractiveTransition:percent];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)checkEdgeInteractive {
|
||||
//TODO: changed the user interactive, if someone else has different requirements, change it.
|
||||
self.handler.screenEdgeGestureRecognizer.enabled = [[self presentable] allowScreenEdgeInteractive];
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (id <HWPanModalPresentable>)presentable {
|
||||
if ([self.presentedViewController conformsToProtocol:@protocol(HWPanModalPresentable)]) {
|
||||
return (id <HWPanModalPresentable>) self.presentedViewController;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (HWPanModalInteractiveAnimator *)interactiveAnimator {
|
||||
HWPanModalPresentationDelegate *presentationDelegate = self.presentedViewController.hw_panModalPresentationDelegate;
|
||||
return presentationDelegate.interactiveDismissalAnimator;
|
||||
}
|
||||
|
||||
- (HWDimmedView *)backgroundView {
|
||||
if (!_backgroundView) {
|
||||
if (self.presentable) {
|
||||
_backgroundView = [[HWDimmedView alloc] initWithBackgroundConfig:[self.presentable backgroundConfig]];
|
||||
} else {
|
||||
_backgroundView = [[HWDimmedView alloc] init];
|
||||
}
|
||||
|
||||
if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
_backgroundView.userInteractionEnabled = NO;
|
||||
} else {
|
||||
__weak typeof(self) wkSelf = self;
|
||||
_backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
|
||||
if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
|
||||
[wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return _backgroundView;
|
||||
}
|
||||
|
||||
- (HWPanContainerView *)panContainerView {
|
||||
if (!_panContainerView) {
|
||||
_panContainerView = [[HWPanContainerView alloc] initWithPresentedView:self.presentedViewController.view frame:self.containerView.frame];
|
||||
}
|
||||
|
||||
return _panContainerView;
|
||||
}
|
||||
|
||||
- (UIView<HWPanModalIndicatorProtocol> *)dragIndicatorView {
|
||||
|
||||
if (!_dragIndicatorView) {
|
||||
if ([self presentable] &&
|
||||
[[self presentable] respondsToSelector:@selector(customIndicatorView)] &&
|
||||
[[self presentable] customIndicatorView] != nil) {
|
||||
_dragIndicatorView = [[self presentable] customIndicatorView];
|
||||
// set the indicator size first in case `setupSubviews` can Not get the right size.
|
||||
_dragIndicatorView.hw_size = [[[self presentable] customIndicatorView] indicatorSize];
|
||||
} else {
|
||||
_dragIndicatorView = [HWPanIndicatorView new];
|
||||
}
|
||||
}
|
||||
|
||||
return _dragIndicatorView;
|
||||
}
|
||||
|
||||
@end
|
||||
29
Pods/HWPanModal/Sources/Delegate/HWPanModalPresentationDelegate.h
generated
Normal file
29
Pods/HWPanModal/Sources/Delegate/HWPanModalPresentationDelegate.h
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// HWPanModalPresentationDelegate.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class HWPanModalInteractiveAnimator;
|
||||
|
||||
typedef NS_ENUM(NSInteger, PanModalInteractiveMode) {
|
||||
PanModalInteractiveModeNone,
|
||||
PanModalInteractiveModeSideslip, // 侧滑返回
|
||||
PanModalInteractiveModeDragDown, // 向下拖拽返回
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanModalPresentationDelegate : NSObject <UIViewControllerTransitioningDelegate, UIAdaptivePresentationControllerDelegate, UIPopoverPresentationControllerDelegate>
|
||||
|
||||
@property (nonatomic, assign) BOOL interactive;
|
||||
@property (nonatomic, assign) PanModalInteractiveMode interactiveMode;
|
||||
@property (nonnull, nonatomic, strong, readonly) HWPanModalInteractiveAnimator *interactiveDismissalAnimator;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
66
Pods/HWPanModal/Sources/Delegate/HWPanModalPresentationDelegate.m
generated
Normal file
66
Pods/HWPanModal/Sources/Delegate/HWPanModalPresentationDelegate.m
generated
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// HWPanModalPresentationDelegate.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
#import "HWPanModalPresentationAnimator.h"
|
||||
#import "HWPanModalPresentationController.h"
|
||||
#import "HWPanModalInteractiveAnimator.h"
|
||||
|
||||
@interface HWPanModalPresentationDelegate ()
|
||||
|
||||
@property (nonatomic, strong) HWPanModalInteractiveAnimator *interactiveDismissalAnimator;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalPresentationDelegate
|
||||
|
||||
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
|
||||
return [[HWPanModalPresentationAnimator alloc] initWithTransitionStyle:TransitionStylePresentation interactiveMode:PanModalInteractiveModeNone];
|
||||
}
|
||||
|
||||
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
|
||||
return [[HWPanModalPresentationAnimator alloc] initWithTransitionStyle:TransitionStyleDismissal interactiveMode:self.interactiveMode];
|
||||
}
|
||||
|
||||
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
|
||||
if (self.interactive) {
|
||||
return self.interactiveDismissalAnimator;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source {
|
||||
UIPresentationController *controller = [[HWPanModalPresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
|
||||
controller.delegate = self;
|
||||
return controller;
|
||||
}
|
||||
|
||||
#pragma mark - UIAdaptivePresentationControllerDelegate
|
||||
|
||||
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection {
|
||||
return UIModalPresentationNone;
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (HWPanModalInteractiveAnimator *)interactiveDismissalAnimator {
|
||||
if (!_interactiveDismissalAnimator) {
|
||||
_interactiveDismissalAnimator = [[HWPanModalInteractiveAnimator alloc] init];
|
||||
}
|
||||
return _interactiveDismissalAnimator;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
- (void)dealloc {
|
||||
NSLog(@"%s", __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
36
Pods/HWPanModal/Sources/HWPanModal.h
generated
Normal file
36
Pods/HWPanModal/Sources/HWPanModal.h
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// HWPanModal.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/4/30.
|
||||
//
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for HWPanModal.
|
||||
FOUNDATION_EXPORT double HWPanModalVersionNumber;
|
||||
|
||||
//! Project version string for JYHitchModule.
|
||||
FOUNDATION_EXPORT const unsigned char HWPanModalVersionString[];
|
||||
|
||||
// protocol
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
#import <HWPanModal/HWPanModalPanGestureDelegate.h>
|
||||
#import <HWPanModal/HWPanModalHeight.h>
|
||||
|
||||
#import <HWPanModal/HWPanModalPresenterProtocol.h>
|
||||
|
||||
// category
|
||||
#import <HWPanModal/UIViewController+PanModalDefault.h>
|
||||
#import <HWPanModal/UIViewController+Presentation.h>
|
||||
#import <HWPanModal/UIViewController+PanModalPresenter.h>
|
||||
|
||||
// custom animation
|
||||
#import <HWPanModal/HWPresentingVCAnimatedTransitioning.h>
|
||||
|
||||
// view
|
||||
#import <HWPanModal/HWPanModalIndicatorProtocol.h>
|
||||
#import <HWPanModal/HWPanIndicatorView.h>
|
||||
#import <HWPanModal/HWDimmedView.h>
|
||||
|
||||
// panModal view
|
||||
#import <HWPanModal/HWPanModalContentView.h>
|
||||
52
Pods/HWPanModal/Sources/KVO/KeyValueObserver.h
generated
Normal file
52
Pods/HWPanModal/Sources/KVO/KeyValueObserver.h
generated
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// KeyValueObserver.h
|
||||
// Lab Color Space Explorer
|
||||
//
|
||||
// Created by Daniel Eggert on 01/12/2013.
|
||||
// Copyright (c) 2013 objc.io. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
|
||||
@interface KeyValueObserver : NSObject
|
||||
|
||||
@property (nonatomic, weak) id target;
|
||||
@property (nonatomic) SEL selector;
|
||||
|
||||
/// Create a Key-Value Observing helper object.
|
||||
///
|
||||
/// As long as the returned token object is retained, the KVO notifications of the @c object
|
||||
/// and @c keyPath will cause the given @c selector to be called on @c target.
|
||||
/// @a object and @a target are weak references.
|
||||
/// Once the token object gets dealloc'ed, the observer gets removed.
|
||||
///
|
||||
/// The @c selector should conform to
|
||||
/// @code
|
||||
/// - (void)nameDidChange:(NSDictionary *)change;
|
||||
/// @endcode
|
||||
/// The passed in dictionary is the KVO change dictionary (c.f. @c NSKeyValueChangeKindKey, @c NSKeyValueChangeNewKey etc.)
|
||||
///
|
||||
/// @returns the opaque token object to be stored in a property
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// @code
|
||||
/// self.nameObserveToken = [KeyValueObserver observeObject:user
|
||||
/// keyPath:@"name"
|
||||
/// target:self
|
||||
/// selector:@selector(nameDidChange:)];
|
||||
/// @endcode
|
||||
+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector __attribute__((warn_unused_result));
|
||||
|
||||
/// Create a key-value-observer with the given KVO options
|
||||
+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector options:(NSKeyValueObservingOptions)options __attribute__((warn_unused_result));
|
||||
|
||||
/**
|
||||
* When you call this method, observer will not work.
|
||||
* Please call observer method again.
|
||||
*/
|
||||
- (void)unObserver;
|
||||
|
||||
@end
|
||||
85
Pods/HWPanModal/Sources/KVO/KeyValueObserver.m
generated
Normal file
85
Pods/HWPanModal/Sources/KVO/KeyValueObserver.m
generated
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// KeyValueObserver.m
|
||||
// Lab Color Space Explorer
|
||||
//
|
||||
// Created by Daniel Eggert on 01/12/2013.
|
||||
// Copyright (c) 2013 objc.io. All rights reserved.
|
||||
//
|
||||
|
||||
#import "KeyValueObserver.h"
|
||||
|
||||
//
|
||||
// Created by chris on 7/24/13.
|
||||
//
|
||||
|
||||
#import "KeyValueObserver.h"
|
||||
|
||||
@interface KeyValueObserver ()
|
||||
@property (nonatomic, weak) id observedObject;
|
||||
@property (nonatomic, copy) NSString* keyPath;
|
||||
@property (nonatomic, assign) BOOL shouldObserver;
|
||||
@end
|
||||
|
||||
@implementation KeyValueObserver
|
||||
|
||||
- (id)initWithObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector options:(NSKeyValueObservingOptions)options;
|
||||
{
|
||||
if (object == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSParameterAssert(target != nil);
|
||||
NSParameterAssert([target respondsToSelector:selector]);
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_shouldObserver = YES;
|
||||
self.target = target;
|
||||
self.selector = selector;
|
||||
self.observedObject = object;
|
||||
self.keyPath = keyPath;
|
||||
[object addObserver:self forKeyPath:keyPath options:options context:(__bridge void *)(self)];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector __attribute__((warn_unused_result));
|
||||
{
|
||||
return [self observeObject:object keyPath:keyPath target:target selector:selector options:0];
|
||||
}
|
||||
|
||||
+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector options:(NSKeyValueObservingOptions)options __attribute__((warn_unused_result));
|
||||
{
|
||||
return [[self alloc] initWithObject:object keyPath:keyPath target:target selector:selector options:options];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
|
||||
{
|
||||
if (context == (__bridge void *)(self)) {
|
||||
[self didChange:change];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didChange:(NSDictionary *)change {
|
||||
|
||||
if (!self.shouldObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
id strongTarget = self.target;
|
||||
|
||||
if ([strongTarget respondsToSelector:self.selector]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[strongTarget performSelector:self.selector withObject:change];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.observedObject removeObserver:self forKeyPath:self.keyPath];
|
||||
}
|
||||
|
||||
- (void)unObserver {
|
||||
self.shouldObserver = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
102
Pods/HWPanModal/Sources/Mediator/HWPanModalPresentableHandler.h
generated
Normal file
102
Pods/HWPanModal/Sources/Mediator/HWPanModalPresentableHandler.h
generated
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// HWPanModalPresentableHandler.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/10/15.
|
||||
// Copyright © 2019 Heath Wang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
#import <HWPanModal/HWPanModalPanGestureDelegate.h>
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, HWPanModalPresentableHandlerMode) {
|
||||
HWPanModalPresentableHandlerModeViewController, // used for UIViewController
|
||||
HWPanModalPresentableHandlerModeView, // used for view
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol HWPanModalPresentableHandlerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* tell the delegate the presentable is about to update origin y
|
||||
*/
|
||||
- (void)adjustPresentableYPos:(CGFloat)yPos;
|
||||
|
||||
/**
|
||||
* tell the delegate presentable is about to change the form state
|
||||
* @param state short,medium, long
|
||||
*/
|
||||
- (void)presentableTransitionToState:(PresentationState)state;
|
||||
|
||||
|
||||
/**
|
||||
* get current CurrentPresentationState of the delegate
|
||||
*/
|
||||
- (PresentationState)getCurrentPresentationState;
|
||||
|
||||
/**
|
||||
* dismiss Controller/UIView
|
||||
* @param isInteractive only for UIViewController, pop view will ignore it.
|
||||
* @param mode only for UIViewController, pop view will ignore it.
|
||||
*/
|
||||
- (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode;
|
||||
|
||||
@optional
|
||||
- (void)cancelInteractiveTransition;
|
||||
- (void)finishInteractiveTransition;
|
||||
|
||||
@end
|
||||
|
||||
@protocol HWPanModalPresentableHandlerDataSource <NSObject>
|
||||
|
||||
- (CGSize)containerSize;
|
||||
- (BOOL)isBeingDismissed;
|
||||
- (BOOL)isBeingPresented;
|
||||
- (BOOL)isFormPositionAnimating;
|
||||
|
||||
@optional
|
||||
- (BOOL)isPresentedViewAnchored;
|
||||
- (BOOL)isPresentedControllerInteractive;
|
||||
|
||||
@end
|
||||
|
||||
@interface HWPanModalPresentableHandler : NSObject <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic, assign, readonly) CGFloat shortFormYPosition;
|
||||
@property (nonatomic, assign, readonly) CGFloat mediumFormYPosition;
|
||||
@property (nonatomic, assign, readonly) CGFloat longFormYPosition;
|
||||
@property (nonatomic, assign, readonly) BOOL extendsPanScrolling;
|
||||
@property (nonatomic, assign, readonly) BOOL anchorModalToLongForm;
|
||||
@property (nonatomic, assign, readonly) CGFloat anchoredYPosition;
|
||||
|
||||
@property (nonatomic, strong, readonly) UIPanGestureRecognizer *panGestureRecognizer;
|
||||
// make controller or view to deal with the gesture action
|
||||
@property (nonatomic, strong, readonly) UIPanGestureRecognizer *screenEdgeGestureRecognizer;
|
||||
|
||||
@property (nonatomic, assign) HWPanModalPresentableHandlerMode mode;
|
||||
@property (nonatomic, weak) UIView<HWPanModalIndicatorProtocol> *dragIndicatorView;
|
||||
@property (nonatomic, weak) UIView *presentedView; // which used to present.
|
||||
|
||||
@property(nonatomic, weak) id <HWPanModalPresentableHandlerDelegate> delegate;
|
||||
@property(nonatomic, weak) id <HWPanModalPresentableHandlerDataSource> dataSource;
|
||||
|
||||
- (instancetype)initWithPresentable:(id <HWPanModalPresentable>)presentable;
|
||||
+ (instancetype)handlerWithPresentable:(id <HWPanModalPresentable>)presentable;
|
||||
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (void)observeScrollable;
|
||||
|
||||
- (void)configureScrollViewInsets;
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated;
|
||||
|
||||
- (void)configureViewLayout;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
769
Pods/HWPanModal/Sources/Mediator/HWPanModalPresentableHandler.m
generated
Normal file
769
Pods/HWPanModal/Sources/Mediator/HWPanModalPresentableHandler.m
generated
Normal file
@@ -0,0 +1,769 @@
|
||||
//
|
||||
// HWPanModalPresentableHandler.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/10/15.
|
||||
// Copyright © 2019 Heath Wang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HWPanModalPresentableHandler.h"
|
||||
#import "UIScrollView+Helper.h"
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
#import "UIView+HW_Frame.h"
|
||||
#import "KeyValueObserver.h"
|
||||
#import "HWPanModalContentView.h"
|
||||
|
||||
static NSString *const kScrollViewKVOContentOffsetKey = @"contentOffset";
|
||||
|
||||
@interface HWPanModalPresentableHandler ()
|
||||
|
||||
@property (nonatomic, assign) CGFloat shortFormYPosition;
|
||||
|
||||
@property (nonatomic, assign) CGFloat mediumFormYPosition;
|
||||
|
||||
@property (nonatomic, assign) CGFloat longFormYPosition;
|
||||
|
||||
@property (nonatomic, assign) BOOL extendsPanScrolling;
|
||||
|
||||
@property (nonatomic, assign) BOOL anchorModalToLongForm;
|
||||
|
||||
@property (nonatomic, assign) CGFloat anchoredYPosition;
|
||||
|
||||
@property (nonatomic, strong) id<HWPanModalPresentable, HWPanModalPanGestureDelegate> presentable;
|
||||
|
||||
// keyboard handle
|
||||
@property (nonatomic, copy) NSDictionary *keyboardInfo;
|
||||
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer;
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *screenEdgeGestureRecognizer;
|
||||
|
||||
// kvo
|
||||
@property (nonatomic, strong) id observerToken;
|
||||
@property (nonatomic, assign) CGFloat scrollViewYOffset;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalPresentableHandler
|
||||
|
||||
- (instancetype)initWithPresentable:(id <HWPanModalPresentable>)presentable {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_presentable = presentable;
|
||||
_extendsPanScrolling = YES;
|
||||
_anchorModalToLongForm = YES;
|
||||
[self addKeyboardObserver];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)handlerWithPresentable:(id <HWPanModalPresentable>)presentable {
|
||||
return [[self alloc] initWithPresentable:presentable];
|
||||
}
|
||||
|
||||
#pragma mark - Pan Gesture Event Handler
|
||||
|
||||
- (void)didPanOnView:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
if ([self shouldResponseToPanGestureRecognizer:panGestureRecognizer] && !self.keyboardInfo) {
|
||||
|
||||
switch (panGestureRecognizer.state) {
|
||||
|
||||
case UIGestureRecognizerStateBegan:
|
||||
case UIGestureRecognizerStateChanged: {
|
||||
[self handlePanGestureBeginOrChanged:panGestureRecognizer];
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
[self handlePanGestureEnded:panGestureRecognizer];
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
} else {
|
||||
[self handlePanGestureDidNotResponse:panGestureRecognizer];
|
||||
}
|
||||
[self.presentable didRespondToPanModalGestureRecognizer:panGestureRecognizer];
|
||||
}
|
||||
|
||||
- (BOOL)shouldResponseToPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
if ([self.presentable shouldRespondToPanModalGestureRecognizer:panGestureRecognizer] ||
|
||||
!(panGestureRecognizer.state == UIGestureRecognizerStateBegan || panGestureRecognizer.state == UIGestureRecognizerStateCancelled)) {
|
||||
|
||||
return ![self shouldFailPanGestureRecognizer:panGestureRecognizer];
|
||||
} else {
|
||||
// stop pan gesture working.
|
||||
panGestureRecognizer.enabled = NO;
|
||||
panGestureRecognizer.enabled = YES;
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)shouldFailPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
if ([self shouldPrioritizePanGestureRecognizer:panGestureRecognizer]) {
|
||||
// high priority than scroll view gesture, disable scrollView gesture.
|
||||
[self.presentable panScrollable].panGestureRecognizer.enabled = NO;
|
||||
[self.presentable panScrollable].panGestureRecognizer.enabled = YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([self shouldHandleShortStatePullDownWithRecognizer:panGestureRecognizer]) {
|
||||
// panGestureRecognizer.enabled = NO;
|
||||
// panGestureRecognizer.enabled = YES;
|
||||
return YES;
|
||||
}
|
||||
|
||||
BOOL shouldFail = NO;
|
||||
UIScrollView *scrollView = [self.presentable panScrollable];
|
||||
if (scrollView) {
|
||||
shouldFail = scrollView.contentOffset.y > -MAX(scrollView.contentInset.top, 0);
|
||||
|
||||
// we want scroll the panScrollable, not the presentedView
|
||||
if (self.isPresentedViewAnchored && shouldFail) {
|
||||
CGPoint location = [panGestureRecognizer locationInView:self.presentedView];
|
||||
BOOL flag = CGRectContainsPoint(scrollView.frame, location) || scrollView.isScrolling;
|
||||
if (flag) {
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
}
|
||||
return flag;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (BOOL)shouldHandleShortStatePullDownWithRecognizer:(UIPanGestureRecognizer *)recognizer {
|
||||
if ([self.presentable allowsPullDownWhenShortState]) return NO;
|
||||
|
||||
CGPoint location = [recognizer translationInView:self.presentedView];
|
||||
if ([self.delegate getCurrentPresentationState] == PresentationStateShort && recognizer.state == UIGestureRecognizerStateBegan) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ((self.presentedView.frame.origin.y >= self.shortFormYPosition || HW_TWO_FLOAT_IS_EQUAL(self.presentedView.frame.origin.y, self.shortFormYPosition)) && location.y > 0) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldPrioritizePanGestureRecognizer:(UIPanGestureRecognizer *)recognizer {
|
||||
return recognizer.state == UIGestureRecognizerStateBegan && [[self presentable] shouldPrioritizePanModalGestureRecognizer:recognizer];
|
||||
}
|
||||
|
||||
- (void)respondToPanGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
[self.presentable willRespondToPanModalGestureRecognizer:panGestureRecognizer];
|
||||
|
||||
CGFloat yDisplacement = [panGestureRecognizer translationInView:self.presentedView].y;
|
||||
|
||||
if (self.presentedView.frame.origin.y < self.longFormYPosition) {
|
||||
yDisplacement = yDisplacement / 2;
|
||||
}
|
||||
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(adjustPresentableYPos:)]) {
|
||||
[delegate adjustPresentableYPos:self.presentedView.frame.origin.y + yDisplacement];
|
||||
}
|
||||
|
||||
[panGestureRecognizer setTranslation:CGPointZero inView:self.presentedView];
|
||||
}
|
||||
|
||||
- (BOOL)isVelocityWithinSensitivityRange:(CGFloat)velocity {
|
||||
return (fabs(velocity) - [self.presentable minVerticalVelocityToTriggerDismiss]) > 0;
|
||||
}
|
||||
|
||||
- (CGFloat)nearestDistance:(CGFloat)position inDistances:(NSArray *)distances {
|
||||
|
||||
if (distances.count <= 0) {
|
||||
return position;
|
||||
}
|
||||
|
||||
// TODO: need refine this sort code.
|
||||
NSMutableArray *tmpArr = [NSMutableArray arrayWithCapacity:distances.count];
|
||||
NSMutableDictionary *tmpDict = [NSMutableDictionary dictionaryWithCapacity:distances.count];
|
||||
|
||||
[distances enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
||||
NSNumber *number = obj;
|
||||
NSNumber *absValue = @(fabs(number.floatValue - position));
|
||||
[tmpArr addObject:absValue];
|
||||
tmpDict[absValue] = number;
|
||||
|
||||
}];
|
||||
|
||||
[tmpArr sortUsingSelector:@selector(compare:)];
|
||||
|
||||
NSNumber *result = tmpDict[tmpArr.firstObject];
|
||||
return result.floatValue;
|
||||
}
|
||||
|
||||
- (void)screenEdgeInteractiveAction:(UIPanGestureRecognizer *)gestureRecognizer {
|
||||
//
|
||||
}
|
||||
|
||||
#pragma mark - handle did Pan gesture events
|
||||
|
||||
- (void)handlePanGestureDidNotResponse:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
switch (panGestureRecognizer.state) {
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
[self cancelInteractiveTransition];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
[panGestureRecognizer setTranslation:CGPointZero inView:panGestureRecognizer.view];
|
||||
}
|
||||
|
||||
- (void)handlePanGestureBeginOrChanged:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
CGPoint velocity = [panGestureRecognizer velocityInView:self.presentedView];
|
||||
[self respondToPanGestureRecognizer:panGestureRecognizer];
|
||||
|
||||
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
|
||||
// check if toggle dismiss action
|
||||
if ([[self presentable] presentingVCAnimationStyle] > PresentingViewControllerAnimationStyleNone &&
|
||||
velocity.y > 0 &&
|
||||
(self.presentedView.frame.origin.y > self.shortFormYPosition || HW_TWO_FLOAT_IS_EQUAL(self.presentedView.frame.origin.y, self.shortFormYPosition))) {
|
||||
[self dismissPresentable:YES mode:PanModalInteractiveModeDragDown];
|
||||
}
|
||||
}
|
||||
|
||||
if (HW_TWO_FLOAT_IS_EQUAL(self.presentedView.frame.origin.y, self.anchoredYPosition) && self.extendsPanScrolling) {
|
||||
[self.presentable willTransitionToState:PresentationStateLong];
|
||||
}
|
||||
|
||||
// update drag indicator
|
||||
if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
|
||||
if (velocity.y > 0) {
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStatePullDown];
|
||||
} else if (velocity.y < 0 && self.presentedView.frame.origin.y <= self.anchoredYPosition && !self.extendsPanScrolling) {
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handlePanGestureEnded:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
CGPoint velocity = [panGestureRecognizer velocityInView:self.presentedView];
|
||||
/**
|
||||
* pan recognizer结束
|
||||
* 根据velocity(速度),当velocity.y < 0,说明用户在向上拖拽view;当velocity.y > 0,向下拖拽
|
||||
* 根据拖拽的速度,处理不同的情况:
|
||||
* 1.超过拖拽速度阈值时并且向下拖拽,dismiss controller
|
||||
* 2.向上拖拽永远不会dismiss,回弹至相应的状态
|
||||
*/
|
||||
|
||||
if ([self isVelocityWithinSensitivityRange:velocity.y]) {
|
||||
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
PresentationState currentState = [delegate getCurrentPresentationState];
|
||||
|
||||
if (velocity.y < 0) {
|
||||
[self handleDragUpState:currentState];
|
||||
} else {
|
||||
[self handleDragDownState:currentState];
|
||||
}
|
||||
} else {
|
||||
CGFloat position = [self nearestDistance:CGRectGetMinY(self.presentedView.frame) inDistances:@[@([self containerSize].height), @(self.shortFormYPosition), @(self.longFormYPosition), @(self.mediumFormYPosition)]];
|
||||
if (HW_TWO_FLOAT_IS_EQUAL(position, self.longFormYPosition)) {
|
||||
[self transitionToState:PresentationStateLong];
|
||||
[self cancelInteractiveTransition];
|
||||
} else if (HW_TWO_FLOAT_IS_EQUAL(position, self.mediumFormYPosition)) {
|
||||
[self transitionToState:PresentationStateMedium];
|
||||
[self cancelInteractiveTransition];
|
||||
} else if (HW_TWO_FLOAT_IS_EQUAL(position, self.shortFormYPosition) || ![self.presentable allowsDragToDismiss]) {
|
||||
[self transitionToState:PresentationStateShort];
|
||||
[self cancelInteractiveTransition];
|
||||
} else {
|
||||
if ([self isBeingDismissed]) {
|
||||
[self finishInteractiveTransition];
|
||||
} else {
|
||||
[self dismissPresentable:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
}
|
||||
}
|
||||
[self.presentable didEndRespondToPanModalGestureRecognizer:panGestureRecognizer];
|
||||
}
|
||||
|
||||
- (void)handleDragUpState:(PresentationState)state {
|
||||
switch (state) {
|
||||
case PresentationStateLong:
|
||||
[self transitionToState:PresentationStateLong];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
case PresentationStateMedium:
|
||||
[self transitionToState:PresentationStateLong];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
case PresentationStateShort:
|
||||
[self transitionToState:PresentationStateMedium];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleDragDownState:(PresentationState)state {
|
||||
switch (state) {
|
||||
case PresentationStateLong:
|
||||
[self transitionToState:PresentationStateMedium];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
case PresentationStateMedium:
|
||||
[self transitionToState:PresentationStateShort];
|
||||
[self cancelInteractiveTransition];
|
||||
break;
|
||||
case PresentationStateShort:
|
||||
if (![self.presentable allowsDragToDismiss]) {
|
||||
[self transitionToState:PresentationStateShort];
|
||||
[self cancelInteractiveTransition];
|
||||
} else {
|
||||
if ([self isBeingDismissed]) {
|
||||
[self finishInteractiveTransition];
|
||||
} else {
|
||||
[self dismissPresentable:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollView kvo
|
||||
|
||||
- (void)observeScrollable {
|
||||
UIScrollView *scrollView = [[self presentable] panScrollable];
|
||||
if (!scrollView) {
|
||||
// force set observerToken to nil, make sure to callback.
|
||||
self.observerToken = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
self.scrollViewYOffset = MAX(scrollView.contentOffset.y, -(MAX(scrollView.contentInset.top, 0)));
|
||||
self.observerToken = [KeyValueObserver observeObject:scrollView keyPath:kScrollViewKVOContentOffsetKey target:self selector:@selector(didPanOnScrollViewChanged:) options:NSKeyValueObservingOptionOld];
|
||||
}
|
||||
|
||||
/**
|
||||
As the user scrolls, track & save the scroll view y offset.
|
||||
This helps halt scrolling when we want to hold the scroll view in place.
|
||||
*/
|
||||
- (void)trackScrolling:(UIScrollView *)scrollView {
|
||||
self.scrollViewYOffset = MAX(scrollView.contentOffset.y, -(MAX(scrollView.contentInset.top, 0)));
|
||||
scrollView.showsVerticalScrollIndicator = [[self presentable] showsScrollableVerticalScrollIndicator];
|
||||
}
|
||||
|
||||
/**
|
||||
* Halts the scroll of a given scroll view & anchors it at the `scrollViewYOffset`
|
||||
*/
|
||||
- (void)haltScrolling:(UIScrollView *)scrollView {
|
||||
|
||||
//
|
||||
// Fix bug: the app will crash after the table view reloads data via calling [tableView reloadData] if the user scrolls to the bottom.
|
||||
//
|
||||
// We remove some element and reload data, for example, [self.dataSource removeLastObject], the previous saved scrollViewYOffset value
|
||||
// will be great than or equal to the current actual offset(i.e. scrollView.contentOffset.y). At this time, if the method
|
||||
// [scrollView setContentOffset:CGPointMake(0, self.scrollViewYOffset) animated:NO] is called, which will trigger KVO recursively.
|
||||
// So scrollViewYOffset must be less than or equal to the actual offset here.
|
||||
// See issues: https://github.com/HeathWang/HWPanModal/issues/107 and https://github.com/HeathWang/HWPanModal/issues/103
|
||||
|
||||
if (scrollView.contentOffset.y <= 0 || self.scrollViewYOffset <= scrollView.contentOffset.y) {
|
||||
[scrollView setContentOffset:CGPointMake(0, self.scrollViewYOffset) animated:NO];
|
||||
scrollView.showsVerticalScrollIndicator = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didPanOnScrollViewChanged:(NSDictionary<NSKeyValueChangeKey, id> *)change {
|
||||
|
||||
UIScrollView *scrollView = [[self presentable] panScrollable];
|
||||
if (!scrollView) return;
|
||||
|
||||
if ((![self isBeingDismissed] && ![self isBeingPresented]) ||
|
||||
([self isBeingDismissed] && [self isPresentedViewControllerInteractive])) {
|
||||
|
||||
if (![self isPresentedViewAnchored] && scrollView.contentOffset.y > 0) {
|
||||
[self haltScrolling:scrollView];
|
||||
} else if ([scrollView isScrolling] || [self isPresentedViewAnimating]) {
|
||||
|
||||
// While we're scrolling upwards on the scrollView, store the last content offset position
|
||||
if ([self isPresentedViewAnchored]) {
|
||||
[self trackScrolling:scrollView];
|
||||
} else {
|
||||
/**
|
||||
* Keep scroll view in place while we're panning on main view
|
||||
*/
|
||||
[self haltScrolling:scrollView];
|
||||
}
|
||||
} else {
|
||||
[self trackScrolling:scrollView];
|
||||
}
|
||||
|
||||
} else {
|
||||
/**
|
||||
* 当present Controller,而且动画没有结束的时候,用户可能会对scrollView设置contentOffset
|
||||
* 首次用户滑动scrollView时,会因为scrollViewYOffset = 0而出现错位
|
||||
*/
|
||||
if ([self isBeingPresented]) {
|
||||
[self setScrollableContentOffset:scrollView.contentOffset animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollView update
|
||||
|
||||
- (void)configureScrollViewInsets {
|
||||
|
||||
// when scrolling, return
|
||||
if ([self.presentable panScrollable] && ![self.presentable panScrollable].isScrolling) {
|
||||
UIScrollView *scrollView = [self.presentable panScrollable];
|
||||
// 禁用scrollView indicator除非用户开始滑动scrollView
|
||||
scrollView.showsVerticalScrollIndicator = [self.presentable showsScrollableVerticalScrollIndicator];
|
||||
scrollView.scrollEnabled = [self.presentable isPanScrollEnabled];
|
||||
scrollView.scrollIndicatorInsets = [self.presentable scrollIndicatorInsets];
|
||||
|
||||
if (![self.presentable shouldAutoSetPanScrollContentInset]) return;
|
||||
|
||||
UIEdgeInsets insets1 = scrollView.contentInset;
|
||||
CGFloat bottomLayoutOffset = [UIApplication sharedApplication].keyWindow.rootViewController.bottomLayoutGuide.length;
|
||||
/*
|
||||
* If scrollView has been set contentInset, and bottom is NOT zero, we won't change it.
|
||||
* If contentInset.bottom is zero, set bottom = bottomLayoutOffset
|
||||
* If scrollView has been set contentInset, BUT the bottom < bottomLayoutOffset, set bottom = bottomLayoutOffset
|
||||
*/
|
||||
if (HW_FLOAT_IS_ZERO(insets1.bottom) || insets1.bottom < bottomLayoutOffset) {
|
||||
|
||||
insets1.bottom = bottomLayoutOffset;
|
||||
scrollView.contentInset = insets1;
|
||||
}
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated {
|
||||
if (![self.presentable panScrollable]) return;
|
||||
|
||||
UIScrollView *scrollView = [self.presentable panScrollable];
|
||||
[self.observerToken unObserver];
|
||||
|
||||
[scrollView setContentOffset:offset animated:animated];
|
||||
// wait for animation finished.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) ((animated ? 0.30 : 0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
[self trackScrolling:scrollView];
|
||||
[self observeScrollable];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - layout
|
||||
|
||||
- (void)configureViewLayout {
|
||||
|
||||
if ([self.presentable isKindOfClass:UIViewController.class]) {
|
||||
UIViewController<HWPanModalPresentable> *layoutPresentable = (UIViewController<HWPanModalPresentable> *) self.presentable;
|
||||
self.shortFormYPosition = layoutPresentable.shortFormYPos;
|
||||
self.mediumFormYPosition = layoutPresentable.mediumFormYPos;
|
||||
self.longFormYPosition = layoutPresentable.longFormYPos;
|
||||
self.anchorModalToLongForm = [layoutPresentable anchorModalToLongForm];
|
||||
self.extendsPanScrolling = [layoutPresentable allowsExtendedPanScrolling];
|
||||
} else if ([self.presentable isKindOfClass:HWPanModalContentView.class]) {
|
||||
HWPanModalContentView<HWPanModalPresentable> *layoutPresentable = (HWPanModalContentView<HWPanModalPresentable> *) self.presentable;
|
||||
self.shortFormYPosition = layoutPresentable.shortFormYPos;
|
||||
self.mediumFormYPosition = layoutPresentable.mediumFormYPos;
|
||||
self.longFormYPosition = layoutPresentable.longFormYPos;
|
||||
self.anchorModalToLongForm = [layoutPresentable anchorModalToLongForm];
|
||||
self.extendsPanScrolling = [layoutPresentable allowsExtendedPanScrolling];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - UIGestureRecognizerDelegate
|
||||
|
||||
/**
|
||||
* ONLY When otherGestureRecognizer is panGestureRecognizer, and target gestureRecognizer is panGestureRecognizer, return YES.
|
||||
*/
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
|
||||
if ([self.presentable respondsToSelector:@selector(hw_gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]) {
|
||||
return [self.presentable hw_gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
|
||||
}
|
||||
|
||||
if ([gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
|
||||
return [otherGestureRecognizer isKindOfClass:UIPanGestureRecognizer.class];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前手势为screenGestureRecognizer时,其他pan recognizer都应该fail
|
||||
*/
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
|
||||
if ([self.presentable respondsToSelector:@selector(hw_gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:)]) {
|
||||
return [self.presentable hw_gestureRecognizer:gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:otherGestureRecognizer];
|
||||
}
|
||||
|
||||
|
||||
if (gestureRecognizer == self.screenEdgeGestureRecognizer && [otherGestureRecognizer isKindOfClass:UIPanGestureRecognizer.class]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
||||
if ([self.presentable respondsToSelector:@selector(hw_gestureRecognizer:shouldRequireFailureOfGestureRecognizer:)]) {
|
||||
return [self.presentable hw_gestureRecognizer:gestureRecognizer shouldRequireFailureOfGestureRecognizer:otherGestureRecognizer];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
|
||||
|
||||
if ([self.presentable respondsToSelector:@selector(hw_gestureRecognizerShouldBegin:)]) {
|
||||
return [self.presentable hw_gestureRecognizerShouldBegin:gestureRecognizer];
|
||||
}
|
||||
|
||||
if (gestureRecognizer == self.screenEdgeGestureRecognizer) {
|
||||
CGPoint velocity = [self.screenEdgeGestureRecognizer velocityInView:self.screenEdgeGestureRecognizer.view];
|
||||
|
||||
if (velocity.x <= 0 || HW_TWO_FLOAT_IS_EQUAL(velocity.x, 0)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// check the distance to left edge
|
||||
CGPoint location = [self.screenEdgeGestureRecognizer locationInView:self.screenEdgeGestureRecognizer.view];
|
||||
CGFloat thresholdDistance = [[self presentable] maxAllowedDistanceToLeftScreenEdgeForPanInteraction];
|
||||
if (thresholdDistance > 0 && location.x > thresholdDistance) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (velocity.x > 0 && HW_TWO_FLOAT_IS_EQUAL(velocity.y, 0)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
//TODO: this logic can be updated later.
|
||||
if (velocity.x > 0 && velocity.x / fabs(velocity.y) > 2) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - UIKeyboard Handle
|
||||
|
||||
- (void)addKeyboardObserver {
|
||||
if ([self.presentable isAutoHandleKeyboardEnabled]) {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeKeyboardObserver {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)keyboardWillShow:(NSNotification *)notification {
|
||||
UIView<UIKeyInput> *currentInput = [self findCurrentTextInputInView:self.presentedView];
|
||||
|
||||
if (!currentInput)
|
||||
return;
|
||||
|
||||
self.keyboardInfo = notification.userInfo;
|
||||
[self updatePanContainerFrameForKeyboard];
|
||||
}
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification {
|
||||
self.keyboardInfo = nil;
|
||||
|
||||
NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||
UIViewAnimationCurve curve = (UIViewAnimationCurve) [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
||||
|
||||
[UIView beginAnimations:nil context:nil];
|
||||
[UIView setAnimationBeginsFromCurrentState:YES];
|
||||
[UIView setAnimationCurve:curve];
|
||||
[UIView setAnimationDuration:duration];
|
||||
|
||||
self.presentedView.transform = CGAffineTransformIdentity;
|
||||
|
||||
[UIView commitAnimations];
|
||||
}
|
||||
|
||||
- (void)updatePanContainerFrameForKeyboard {
|
||||
if (!self.keyboardInfo)
|
||||
return;
|
||||
|
||||
UIView<UIKeyInput> *textInput = [self findCurrentTextInputInView:self.presentedView];
|
||||
if (!textInput)
|
||||
return;
|
||||
|
||||
CGAffineTransform lastTransform = self.presentedView.transform;
|
||||
self.presentedView.transform = CGAffineTransformIdentity;
|
||||
|
||||
CGFloat textViewBottomY = [textInput convertRect:textInput.bounds toView:self.presentedView].origin.y + textInput.hw_height;
|
||||
CGFloat keyboardHeight = [self.keyboardInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
|
||||
|
||||
CGFloat offsetY = 0;
|
||||
CGFloat top = [self.presentable keyboardOffsetFromInputView];
|
||||
offsetY = self.presentedView.hw_height - (keyboardHeight + top + textViewBottomY + self.presentedView.hw_top);
|
||||
|
||||
NSTimeInterval duration = [self.keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||
UIViewAnimationCurve curve = (UIViewAnimationCurve) [self.keyboardInfo[UIKeyboardAnimationCurveUserInfoKey] intValue];
|
||||
|
||||
self.presentedView.transform = lastTransform;
|
||||
[UIView beginAnimations:nil context:NULL];
|
||||
[UIView setAnimationBeginsFromCurrentState:YES];
|
||||
[UIView setAnimationCurve:curve];
|
||||
[UIView setAnimationDuration:duration];
|
||||
|
||||
self.presentedView.transform = CGAffineTransformMakeTranslation(0, offsetY);
|
||||
|
||||
[UIView commitAnimations];
|
||||
}
|
||||
|
||||
- (UIView <UIKeyInput> *)findCurrentTextInputInView:(UIView *)view {
|
||||
if ([view conformsToProtocol:@protocol(UIKeyInput)] && view.isFirstResponder) {
|
||||
// Quick fix for web view issue
|
||||
if ([view isKindOfClass:NSClassFromString(@"UIWebBrowserView")] || [view isKindOfClass:NSClassFromString(@"WKContentView")]) {
|
||||
return nil;
|
||||
}
|
||||
return (UIView <UIKeyInput> *) view;
|
||||
}
|
||||
|
||||
for (UIView *subview in view.subviews) {
|
||||
UIView <UIKeyInput> *inputInView = [self findCurrentTextInputInView:subview];
|
||||
if (inputInView) {
|
||||
return inputInView;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - delegate throw
|
||||
|
||||
- (void)transitionToState:(PresentationState)state {
|
||||
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(presentableTransitionToState:)]) {
|
||||
[delegate presentableTransitionToState:state];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelInteractiveTransition {
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(cancelInteractiveTransition)]) {
|
||||
[delegate cancelInteractiveTransition];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)finishInteractiveTransition {
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(finishInteractiveTransition)]) {
|
||||
[delegate finishInteractiveTransition];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dismissPresentable:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode {
|
||||
id <HWPanModalPresentableHandlerDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(dismiss:mode:)]) {
|
||||
[delegate dismiss:isInteractive mode:mode];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - dataSource handle
|
||||
|
||||
- (BOOL)isPresentedViewAnchored {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isPresentedViewAnchored)]) {
|
||||
return [self.dataSource isPresentedViewAnchored];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingDismissed {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isBeingDismissed)]) {
|
||||
return [self.dataSource isBeingDismissed];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingPresented {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isBeingPresented)]) {
|
||||
return [self.dataSource isBeingPresented];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedViewControllerInteractive {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isPresentedControllerInteractive)]) {
|
||||
return [self.dataSource isPresentedControllerInteractive];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedViewAnimating {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(isFormPositionAnimating)]) {
|
||||
[self.dataSource isFormPositionAnimating];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (CGSize)containerSize {
|
||||
if (self.dataSource && [self.dataSource respondsToSelector:@selector(containerSize)]) {
|
||||
return [self.dataSource containerSize];
|
||||
}
|
||||
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
if (!_panGestureRecognizer) {
|
||||
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPanOnView:)];
|
||||
_panGestureRecognizer.minimumNumberOfTouches = 1;
|
||||
_panGestureRecognizer.maximumNumberOfTouches = 1;
|
||||
_panGestureRecognizer.delegate = self;
|
||||
}
|
||||
return _panGestureRecognizer;
|
||||
}
|
||||
|
||||
- (UIPanGestureRecognizer *)screenEdgeGestureRecognizer {
|
||||
if (!_screenEdgeGestureRecognizer) {
|
||||
_screenEdgeGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgeInteractiveAction:)];
|
||||
_screenEdgeGestureRecognizer.minimumNumberOfTouches = 1;
|
||||
_screenEdgeGestureRecognizer.maximumNumberOfTouches = 1;
|
||||
_screenEdgeGestureRecognizer.delegate = self;
|
||||
}
|
||||
|
||||
return _screenEdgeGestureRecognizer;
|
||||
}
|
||||
|
||||
- (CGFloat)anchoredYPosition {
|
||||
CGFloat defaultTopOffset = [self.presentable topOffset];
|
||||
return self.anchorModalToLongForm ? self.longFormYPosition : defaultTopOffset;
|
||||
}
|
||||
|
||||
#pragma mark - Dealloc
|
||||
|
||||
- (void)dealloc {
|
||||
[self removeKeyboardObserver];
|
||||
}
|
||||
|
||||
@end
|
||||
48
Pods/HWPanModal/Sources/Presentable/HWPanModalHeight.h
generated
Normal file
48
Pods/HWPanModal/Sources/Presentable/HWPanModalHeight.h
generated
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// HWPanModalHeight.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, PanModalHeightType) {
|
||||
PanModalHeightTypeMax NS_SWIFT_NAME(max), // from top max
|
||||
PanModalHeightTypeMaxTopInset NS_SWIFT_NAME(topInset), // from top offset
|
||||
PanModalHeightTypeContent NS_SWIFT_NAME(content), // from bottom
|
||||
PanModalHeightTypeContentIgnoringSafeArea NS_SWIFT_NAME(contentIgnoringSafeArea), // from bottom ignore safeArea
|
||||
PanModalHeightTypeIntrinsic NS_SWIFT_NAME(intrinsic), // auto get size, There is something wrong, DO NOT recommend to use.
|
||||
};
|
||||
|
||||
struct PanModalHeight {
|
||||
PanModalHeightType heightType NS_SWIFT_NAME(type);
|
||||
CGFloat height;
|
||||
};
|
||||
|
||||
typedef struct PanModalHeight PanModalHeight;
|
||||
|
||||
/**
|
||||
* When heightType is PanModalHeightTypeMax, PanModalHeightTypeIntrinsic, the height value will be ignored.
|
||||
*/
|
||||
CG_INLINE PanModalHeight PanModalHeightMake(PanModalHeightType heightType, CGFloat height) {
|
||||
PanModalHeight modalHeight;
|
||||
modalHeight.heightType = heightType;
|
||||
modalHeight.height = height;
|
||||
return modalHeight;
|
||||
}
|
||||
|
||||
static inline BOOL HW_FLOAT_IS_ZERO(CGFloat value) {
|
||||
return (value > -FLT_EPSILON) && (value < FLT_EPSILON);
|
||||
}
|
||||
|
||||
static inline BOOL HW_TWO_FLOAT_IS_EQUAL(CGFloat x, CGFloat y) {
|
||||
CGFloat minusValue = fabs(x - y);
|
||||
CGFloat criticalValue = 0.0001;
|
||||
if (minusValue < criticalValue || minusValue < FLT_MIN) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
31
Pods/HWPanModal/Sources/Presentable/HWPanModalPanGestureDelegate.h
generated
Normal file
31
Pods/HWPanModal/Sources/Presentable/HWPanModalPanGestureDelegate.h
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// HWPanModalPanGestureDelegate.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2022/8/1.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* In this framewok, we use UIPanGestureRecognizer to control user drags behavior.
|
||||
* The internal logic, there are two panGestureRecognizers delegate will response below delegate: the main panGesture used to control darg down, another panGesture used to control screen edge dismiss.
|
||||
* Implement this delegate and custom user drag behavior.
|
||||
* WARNING: BE CAREFUL, AND KNOW WHAT YOU ARE DOING!
|
||||
*/
|
||||
@protocol HWPanModalPanGestureDelegate <NSObject>
|
||||
|
||||
- (BOOL)hw_gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
|
||||
- (BOOL)hw_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
|
||||
|
||||
- (BOOL)hw_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
|
||||
- (BOOL)hw_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
451
Pods/HWPanModal/Sources/Presentable/HWPanModalPresentable.h
generated
Normal file
451
Pods/HWPanModal/Sources/Presentable/HWPanModalPresentable.h
generated
Normal file
@@ -0,0 +1,451 @@
|
||||
//
|
||||
// HWPanModalPresentable.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalHeight.h>
|
||||
#import <HWPanModal/HWPresentingVCAnimatedTransitioning.h>
|
||||
#import <HWPanModal/HWPanModalIndicatorProtocol.h>
|
||||
#import <HWPanModal/HWBackgroundConfig.h>
|
||||
#import <HWPanModal/HWPanModalShadow.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, PresentationState) {
|
||||
PresentationStateShort NS_SWIFT_NAME(short),
|
||||
PresentationStateMedium NS_SWIFT_NAME(medium),
|
||||
PresentationStateLong NS_SWIFT_NAME(long),
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, PresentingViewControllerAnimationStyle) {
|
||||
// no animation for presentingVC
|
||||
PresentingViewControllerAnimationStyleNone NS_SWIFT_NAME(none),
|
||||
// page sheet animation, like iOS13 default modalPresentation style
|
||||
PresentingViewControllerAnimationStylePageSheet NS_SWIFT_NAME(pageSheet),
|
||||
// shopping cart animation, like jd/taobao shopping cart animation
|
||||
PresentingViewControllerAnimationStyleShoppingCart NS_SWIFT_NAME(shoppingCart),
|
||||
// make your own custom animation
|
||||
PresentingViewControllerAnimationStyleCustom NS_SWIFT_NAME(custom),
|
||||
};
|
||||
|
||||
/**
|
||||
* HWPanModalPresentable为present配置协议
|
||||
* 默认情况下无需实现,只需Controller/View适配该协议
|
||||
* 通过category来默认实现以下所有方法,避免继承类
|
||||
*
|
||||
* This Protocol is the core of HWPanModal, we use it to config presentation.
|
||||
* Default, you don't need to conform all of these methods, just implement what you want to customize.
|
||||
* All the config has default value, we use a `UIViewController` category to conform `HWPanModalPresentable` protocol.
|
||||
*/
|
||||
@protocol HWPanModalPresentable <NSObject>
|
||||
|
||||
#pragma mark - ScrollView Config
|
||||
|
||||
/**
|
||||
* 支持同步拖拽的scrollView
|
||||
* 如果ViewController中包含scrollView并且你想scrollView滑动和拖拽手势同时存在,请返回此scrollView
|
||||
*
|
||||
* If your ViewController has a scrollable view(UIScrollView and subclass), and you want pan gesture and scrollable both work, return it.
|
||||
*/
|
||||
- (nullable UIScrollView *)panScrollable;
|
||||
|
||||
/**
|
||||
* determine ScrollView scrollEnabled
|
||||
* default is YES
|
||||
*/
|
||||
- (BOOL)isPanScrollEnabled;
|
||||
|
||||
/**
|
||||
* scrollView指示器insets
|
||||
* Use `panModalSetNeedsLayoutUpdate()` when updating insets.
|
||||
*/
|
||||
- (UIEdgeInsets)scrollIndicatorInsets;
|
||||
|
||||
/**
|
||||
* A Boolean value that controls whether the scrollable vertical scroll indicator is visible.
|
||||
* default is YES.
|
||||
*/
|
||||
- (BOOL)showsScrollableVerticalScrollIndicator;
|
||||
|
||||
/**
|
||||
* default is YES.
|
||||
*/
|
||||
- (BOOL)shouldAutoSetPanScrollContentInset;
|
||||
|
||||
/**
|
||||
* 是否允许拖动额外拖动,如果panScrollable存在,且scrollView contentSize > (size + bottomLayoutOffset),返回YES
|
||||
* 其余情况返回NO
|
||||
*
|
||||
* If panScrollable exists, and scrollView contentSize > (size + bottomLayoutOffset), auto return YES, otherwise return NO.
|
||||
* You can make your own logic if you want, and you know what you are doing.
|
||||
*/
|
||||
- (BOOL)allowsExtendedPanScrolling;
|
||||
|
||||
#pragma mark - Offset/position
|
||||
|
||||
/**
|
||||
* Screen top offset from presented viewController
|
||||
* Default is topLayoutGuide.length + 21.0.
|
||||
*/
|
||||
- (CGFloat)topOffset;
|
||||
|
||||
/**
|
||||
* 当pan状态为short时候的高度
|
||||
* default: shortFormHeight = longFormHeight
|
||||
*/
|
||||
- (PanModalHeight)shortFormHeight;
|
||||
|
||||
/**
|
||||
* default: mediumFormHeight = longFormHeight
|
||||
*/
|
||||
- (PanModalHeight)mediumFormHeight;
|
||||
|
||||
/**
|
||||
* 当pan状态为long的高度
|
||||
*/
|
||||
- (PanModalHeight)longFormHeight;
|
||||
|
||||
/**
|
||||
* 初始弹出高度状态,默认为`shortFormHeight`
|
||||
*
|
||||
* Origin presentation height state, if you have special requirement, change it.
|
||||
* Default is `shortFormHeight`
|
||||
*/
|
||||
- (PresentationState)originPresentationState;
|
||||
|
||||
#pragma mark - Animation config
|
||||
|
||||
/**
|
||||
* spring弹性动画数值
|
||||
* Default is 0.9
|
||||
*/
|
||||
- (CGFloat)springDamping;
|
||||
|
||||
/**
|
||||
* 转场动画时间
|
||||
* Default is 0.5 second
|
||||
*/
|
||||
- (NSTimeInterval)transitionDuration;
|
||||
|
||||
/**
|
||||
* starting from version 0.6.5, Only works when dismiss
|
||||
* Default is same as `- (NSTimeInterval)transitionDuration;`
|
||||
*/
|
||||
- (NSTimeInterval)dismissalDuration;
|
||||
|
||||
/**
|
||||
* 转场动画options
|
||||
* Default is UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState
|
||||
*/
|
||||
- (UIViewAnimationOptions)transitionAnimationOptions;
|
||||
|
||||
#pragma mark - AppearanceTransition
|
||||
|
||||
/**
|
||||
* If enabled, the presenting VC will invoke viewWillAppear:, viewWillDisappear:
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)shouldEnableAppearanceTransition;
|
||||
|
||||
#pragma mark - Background config
|
||||
|
||||
/**
|
||||
* use this object to config background alpha or blur effect
|
||||
* @return background config object
|
||||
*/
|
||||
- (HWBackgroundConfig *)backgroundConfig;
|
||||
|
||||
#pragma mark - User Interaction
|
||||
|
||||
/**
|
||||
* 该bool值控制当pan View状态为long的情况下,是否可以继续拖拽到PanModalHeight = MAX的情况
|
||||
* 默认为YES,即当已经拖拽到long的情况下不能再继续拖动
|
||||
*/
|
||||
- (BOOL)anchorModalToLongForm;
|
||||
|
||||
/**
|
||||
* 是否允许点击背景处dismiss presented Controller
|
||||
* 默认为YES
|
||||
*/
|
||||
- (BOOL)allowsTapBackgroundToDismiss;
|
||||
|
||||
|
||||
/**
|
||||
* 是否允许drag操作dismiss presented Controller
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)allowsDragToDismiss;
|
||||
|
||||
/// Default is YES, When return NO, and you did set shortForm, user CAN NOT pull down the view.
|
||||
- (BOOL)allowsPullDownWhenShortState;
|
||||
|
||||
/**
|
||||
min Velocity from Vertical direction that trigger dismiss action.
|
||||
Default is 300.0
|
||||
*/
|
||||
- (CGFloat)minVerticalVelocityToTriggerDismiss;
|
||||
|
||||
/**
|
||||
* 是否允许用户操作
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)isUserInteractionEnabled;
|
||||
|
||||
/**
|
||||
* 是否允许触觉反馈
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)isHapticFeedbackEnabled;
|
||||
|
||||
/**
|
||||
* 是否允许触摸事件透传到presenting ViewController/View。如果你有特殊需求的话(比如弹出一个底部视图,但是你想操作弹出视图下面的view,即presenting VC/View),可开启此功能
|
||||
*
|
||||
* Whether allows touch events passing through the transition container view.
|
||||
* In some situations, you present the bottom VC/View, and you want to operate the presenting VC/View(mapView, scrollView and etc), enable this func.
|
||||
*
|
||||
* Note: You SHOULD MUST dismiss the presented VC in the right time.
|
||||
*/
|
||||
- (BOOL)allowsTouchEventsPassingThroughTransitionView;
|
||||
|
||||
#pragma mark - Screen left egde interaction
|
||||
|
||||
/**
|
||||
* 是否允许屏幕边缘侧滑手势
|
||||
* Default is NO,not allowed this user interaction.
|
||||
*
|
||||
* Note: Currently only works on UIViewController.
|
||||
*/
|
||||
- (BOOL)allowScreenEdgeInteractive;
|
||||
|
||||
/**
|
||||
* Max allowed distance to screen left edge when you want to make screen edge pan interaction
|
||||
* Default is 0, means it will ignore this limit, full screen left edge pan will work.
|
||||
* @return distance to left screen edge
|
||||
*/
|
||||
- (CGFloat)maxAllowedDistanceToLeftScreenEdgeForPanInteraction;
|
||||
|
||||
/**
|
||||
* When you enabled `- (BOOL)allowScreenEdgeInteractive`, this can work.
|
||||
* min horizontal velocity to trigger screen edge dismiss if the drag didn't reach 0.5 screen width.
|
||||
* Default is 500
|
||||
*/
|
||||
- (CGFloat)minHorizontalVelocityToTriggerScreenEdgeDismiss;
|
||||
|
||||
#pragma mark - Customize presentingViewController animation
|
||||
|
||||
/**
|
||||
* Config presentingViewController animation style, this animations will work for present & dismiss.
|
||||
* Default is `PresentingViewControllerAnimationStyleNone`.
|
||||
* @return The animation style.
|
||||
*/
|
||||
- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle;
|
||||
|
||||
/**
|
||||
* 自定义presenting ViewController转场动画,默认为nil
|
||||
* 注意:如果实现该方法并返回非空示例,要使该方法生效,`- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle`必须返回PresentingViewControllerAnimationStyleCustom
|
||||
*
|
||||
* custom presenting ViewController transition animation, default is nil
|
||||
* Note: If you implement this method and return non nil value, You must implement `- (PresentingViewControllerAnimationStyle)
|
||||
* presentingVCAnimationStyle` and return PresentingViewControllerAnimationStyleCustom
|
||||
*/
|
||||
- (nullable id<HWPresentingViewControllerAnimatedTransitioning>)customPresentingVCAnimation;
|
||||
|
||||
#pragma mark - Content UI config
|
||||
|
||||
/**
|
||||
* 是否顶部圆角
|
||||
* Default is YES
|
||||
*/
|
||||
- (BOOL)shouldRoundTopCorners;
|
||||
|
||||
/**
|
||||
* 顶部圆角数值
|
||||
* Default is 8.0
|
||||
*/
|
||||
- (CGFloat)cornerRadius;
|
||||
|
||||
/**
|
||||
* presented content shadow
|
||||
* Default is None config
|
||||
*/
|
||||
- (HWPanModalShadow *)contentShadow;
|
||||
|
||||
#pragma mark - Indicator config
|
||||
|
||||
/**
|
||||
* 是否显示drag指示view
|
||||
* Default is YES,Default this method depend on `- (BOOL)shouldRoundTopCorners`
|
||||
*/
|
||||
- (BOOL)showDragIndicator;
|
||||
|
||||
/**
|
||||
* You can make the indicator customized. Just adopt `HWPanModalIndicatorProtocol`
|
||||
* Default this method return nil, Then the default indicator will be used.
|
||||
*/
|
||||
- (__kindof UIView<HWPanModalIndicatorProtocol> * _Nullable)customIndicatorView;
|
||||
|
||||
#pragma mark - Keyboard handle
|
||||
|
||||
/**
|
||||
* When there is text input view exists and becomeFirstResponder, will auto handle keyboard height.
|
||||
* Default is YES. You can disable it, handle it by yourself.
|
||||
*/
|
||||
- (BOOL)isAutoHandleKeyboardEnabled;
|
||||
|
||||
/**
|
||||
The offset that keyboard show from input view's bottom. It works when
|
||||
`isAutoHandleKeyboardEnabled` return YES.
|
||||
|
||||
@return offset, default is 5.
|
||||
*/
|
||||
- (CGFloat)keyboardOffsetFromInputView;
|
||||
|
||||
#pragma mark - Delegate
|
||||
|
||||
#pragma mark - Pan Gesture delegate
|
||||
|
||||
/**
|
||||
* 询问delegate是否需要使拖拽手势生效
|
||||
* 若返回NO,则禁用拖拽手势操作,即不能拖拽dismiss
|
||||
* 默认为YES
|
||||
*/
|
||||
- (BOOL)shouldRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* 当pan recognizer状态为begin/changed时,通知delegate回调。
|
||||
* 当拖动presented View时,该方法会持续的回调
|
||||
* 默认实现为空
|
||||
*/
|
||||
- (void)willRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* 内部处理完成拖动操作后触发此回调,此时view frame可能已经变化。
|
||||
* Framework has did finish logic for GestureRecognizer delegate. It will call many times when you darg.
|
||||
*/
|
||||
- (void)didRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* 内部处理完成拖动操作后触发此回调,此时view frame可能已经变化。
|
||||
* Framework has did finish logic for GestureRecognizer delegate. It will call many times when you darg.
|
||||
*/
|
||||
- (void)didEndRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* 是否优先执行dismiss拖拽手势,当存在panScrollable的情况下,如果此方法返回YES,则
|
||||
* dismiss手势生效,scrollView本身的滑动则不再生效。也就是说可以拖动Controller view,而scrollView没法拖动了。
|
||||
*
|
||||
* 例子:controller view上添加一个TableView,并铺满全屏,然后在controller view 顶部添加一个一定大小的viewA,
|
||||
* 这个时候会发现viewA有时候无法拖动,可以实现此delegate方法来解决
|
||||
```
|
||||
- (BOOL)shouldPrioritizePanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
CGPoint loc = [panGestureRecognizer locationInView:self.view];
|
||||
// check whether user pan action in viewA
|
||||
if (CGRectContainsPoint(self.viewA.frame, loc)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
```
|
||||
* 默认为NO
|
||||
*
|
||||
* This delegate is useful when you want panGestureRecognizer has a high prioritize and
|
||||
* make scrollable does NOT scroll.
|
||||
* Example: You controller add a full size tableView, then add viewA on top of your controller view.
|
||||
* Now you find you can not drag the viewA, use this delegate to resolve problem.
|
||||
* Please refer to code above this comment.
|
||||
*
|
||||
* Default is NO
|
||||
*/
|
||||
- (BOOL)shouldPrioritizePanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer;
|
||||
|
||||
/**
|
||||
* When you pan present controller to dismiss, and the view's y <= shortFormYPos,
|
||||
* this delegate method will be called.
|
||||
* @param percent 0 ~ 1, 1 means has dismissed
|
||||
*/
|
||||
- (void)panModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer dismissPercent:(CGFloat)percent;
|
||||
|
||||
#pragma mark - PresentationState change delegate
|
||||
/**
|
||||
* 是否应该变更panModal状态
|
||||
*/
|
||||
- (BOOL)shouldTransitionToState:(PresentationState)state;
|
||||
|
||||
/**
|
||||
* called when the Transition State will change.
|
||||
* 通知回调即将变更状态
|
||||
*/
|
||||
- (void)willTransitionToState:(PresentationState)state;
|
||||
|
||||
/**
|
||||
* PresentationState did change callback
|
||||
*/
|
||||
- (void)didChangeTransitionToState:(PresentationState)state;
|
||||
|
||||
#pragma mark - present delegate
|
||||
|
||||
/**
|
||||
* call when present transition will begin.
|
||||
*/
|
||||
- (void)panModalTransitionWillBegin;
|
||||
|
||||
/**
|
||||
* call when present transition did finish.
|
||||
*/
|
||||
- (void)panModalTransitionDidFinish;
|
||||
|
||||
/**
|
||||
* call when your custom presented vc has been added to the presentation container.
|
||||
*/
|
||||
- (void)presentedViewDidMoveToSuperView;
|
||||
|
||||
#pragma mark - Dismiss delegate
|
||||
/**
|
||||
* will dismiss
|
||||
*/
|
||||
- (void)panModalWillDismiss;
|
||||
|
||||
/**
|
||||
* Did finish dismissing
|
||||
*/
|
||||
- (void)panModalDidDismissed;
|
||||
|
||||
#pragma mark - DEPRECATED DECLARE
|
||||
|
||||
/**
|
||||
* 是否对presentingViewController做动画效果,默认该效果类似淘宝/京东购物车凹陷效果
|
||||
* 默认为NO
|
||||
*/
|
||||
- (BOOL)shouldAnimatePresentingVC DEPRECATED_MSG_ATTRIBUTE("This api has been marked as DEPRECATED on version 0.3.6, please use `- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle` replaced.");
|
||||
|
||||
/**
|
||||
* 背景透明度
|
||||
* Default is 0.7
|
||||
*/
|
||||
- (CGFloat)backgroundAlpha DEPRECATED_MSG_ATTRIBUTE("This api has been marked as DEPRECATED on version 0.7.0, please use `- (HWBackgroundConfig *)backgroundConfig` replaced.");
|
||||
|
||||
/**
|
||||
* Blur background
|
||||
* This function can NOT coexist with backgroundAlpha
|
||||
* Default use backgroundAlpha, Once you set backgroundBlurRadius > 0, blur will work.
|
||||
* Recommend set the value 10 ~ 20.
|
||||
* @return blur radius
|
||||
*/
|
||||
- (CGFloat)backgroundBlurRadius DEPRECATED_MSG_ATTRIBUTE("This api has been marked as DEPRECATED on version 0.7.0, please use `- (HWBackgroundConfig *)backgroundConfig` replaced.");
|
||||
|
||||
/**
|
||||
* blur background color
|
||||
* @return color, default is White Color.
|
||||
*/
|
||||
- (nonnull UIColor *)backgroundBlurColor DEPRECATED_MSG_ATTRIBUTE("This api has been marked as DEPRECATED on version 0.7.0, please use `- (HWBackgroundConfig *)backgroundConfig` replaced.");
|
||||
|
||||
@end
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
63
Pods/HWPanModal/Sources/Presentable/HWPanModalPresentationUpdateProtocol.h
generated
Normal file
63
Pods/HWPanModal/Sources/Presentable/HWPanModalPresentationUpdateProtocol.h
generated
Normal file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// HWPanModalPresentationUpdateProtocol.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/17.
|
||||
//
|
||||
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
@class HWDimmedView;
|
||||
|
||||
@protocol HWPanModalPresentationUpdateProtocol <NSObject>
|
||||
/// background view, you can call `reloadConfig:` to update the UI.
|
||||
@property (nonatomic, readonly) HWDimmedView *hw_dimmedView;
|
||||
/// the root container which your custom VC's view to be added.
|
||||
@property (nonatomic, readonly) UIView *hw_rootContainerView;
|
||||
/// which view that your presented viewController's view has been added.
|
||||
@property (nonatomic, readonly) UIView *hw_contentView;
|
||||
/// current presentation State
|
||||
@property (nonatomic, readonly) PresentationState hw_presentationState;
|
||||
/**
|
||||
* force update pan modal State, short/long
|
||||
*/
|
||||
- (void)hw_panModalTransitionTo:(PresentationState)state NS_SWIFT_NAME(panModalTransitionTo(state:));
|
||||
|
||||
/**
|
||||
* force update pan modal State, short/long
|
||||
* @param state transition state
|
||||
* @param animated whether animate when set state
|
||||
*/
|
||||
- (void)hw_panModalTransitionTo:(PresentationState)state animated:(BOOL)animated NS_SWIFT_NAME(panModalTransitionTo(state:animated:));
|
||||
|
||||
/**
|
||||
* When presented ViewController has a UIScrollView, Use This method to update UIScrollView contentOffset
|
||||
* Default it has animation
|
||||
*/
|
||||
- (void)hw_panModalSetContentOffset:(CGPoint)offset NS_SWIFT_NAME(panModalSetContentOffset(offset:));
|
||||
|
||||
/**
|
||||
* When presented ViewController has a UIScrollView, Use This method to update UIScrollView contentOffset
|
||||
* @param offset scrollView offset value
|
||||
* @param animated whether animate
|
||||
*/
|
||||
- (void)hw_panModalSetContentOffset:(CGPoint)offset animated:(BOOL)animated NS_SWIFT_NAME(panModalSetContentOffset(offset:animated:));
|
||||
|
||||
/**
|
||||
* Note:if we present a NavigationController, and we want to pan screen edge to dismiss.
|
||||
* We MUST call this method when we PUSH/POP viewController.
|
||||
*
|
||||
*/
|
||||
- (void)hw_panModalSetNeedsLayoutUpdate NS_SWIFT_NAME(panModalSetNeedsLayoutUpdate());
|
||||
|
||||
/**
|
||||
* When you change the user touch event, like `allowsTouchEventsPassingThroughTransitionView`, you should call this method to make it work.
|
||||
* 更新用户行为,比如事件穿透
|
||||
*/
|
||||
- (void)hw_panModalUpdateUserHitBehavior NS_SWIFT_NAME(panModalUpdateUserHitBehavior());
|
||||
|
||||
/**
|
||||
* call this method to dismiss your presented VC directly
|
||||
*/
|
||||
- (void)hw_dismissAnimated:(BOOL)animated completion:(void (^)(void))completion NS_SWIFT_NAME(panModalDismissAnimated(animated:completion:));
|
||||
|
||||
@end
|
||||
38
Pods/HWPanModal/Sources/Presentable/UIViewController+LayoutHelper.h
generated
Normal file
38
Pods/HWPanModal/Sources/Presentable/UIViewController+LayoutHelper.h
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// UIViewController+LayoutHelper.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@class HWPanModalPresentationController;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol HWPanModalPresentableLayoutProtocol <NSObject>
|
||||
|
||||
@property (nonatomic, assign, readonly) CGFloat topLayoutOffset;
|
||||
|
||||
@property (nonatomic, assign, readonly) CGFloat bottomLayoutOffset;
|
||||
|
||||
@property (nonatomic, assign, readonly) CGFloat shortFormYPos;
|
||||
|
||||
@property (nonatomic, assign, readonly) CGFloat mediumFormYPos;
|
||||
|
||||
@property (nonatomic, assign, readonly) CGFloat longFormYPos;
|
||||
|
||||
@property (nonatomic, assign, readonly) CGFloat bottomYPos;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* Help presentedViewController/presented View to layout.
|
||||
*/
|
||||
@interface UIViewController (LayoutHelper) <HWPanModalPresentableLayoutProtocol>
|
||||
|
||||
@property (nullable, nonatomic, strong, readonly) HWPanModalPresentationController *hw_presentedVC;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
130
Pods/HWPanModal/Sources/Presentable/UIViewController+LayoutHelper.m
generated
Normal file
130
Pods/HWPanModal/Sources/Presentable/UIViewController+LayoutHelper.m
generated
Normal file
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// UIViewController+LayoutHelper.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
#import "HWPanModalPresentationController.h"
|
||||
#import "UIViewController+PanModalDefault.h"
|
||||
|
||||
@implementation UIViewController (LayoutHelper)
|
||||
|
||||
- (CGFloat)topLayoutOffset {
|
||||
if (@available(iOS 11, *)) {
|
||||
return [UIApplication sharedApplication].keyWindow.safeAreaInsets.top;
|
||||
} else {
|
||||
return [UIApplication sharedApplication].keyWindow.rootViewController.topLayoutGuide.length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (CGFloat)bottomLayoutOffset {
|
||||
if (@available(iOS 11, *)) {
|
||||
return [UIApplication sharedApplication].keyWindow.safeAreaInsets.bottom;
|
||||
} else {
|
||||
return [UIApplication sharedApplication].keyWindow.rootViewController.bottomLayoutGuide.length;
|
||||
}
|
||||
}
|
||||
|
||||
- (HWPanModalPresentationController *)hw_presentedVC {
|
||||
/*
|
||||
* Fix iOS13 bug: if we access presentationController before present VC, this will lead `modalPresentationStyle` not working.
|
||||
* refer to: https://github.com/HeathWang/HWPanModal/issues/27
|
||||
* Apple Doc: If you have not yet presented the current view controller, accessing this property creates a presentation controller based on the current value in the modalPresentationStyle property.
|
||||
*/
|
||||
|
||||
/**
|
||||
* fix bug: https://github.com/HeathWang/HWPanModal/issues/37
|
||||
*/
|
||||
if (self.presentingViewController) {
|
||||
return [self hw_getPanModalPresentationController];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (HWPanModalPresentationController *)hw_getPanModalPresentationController {
|
||||
UIViewController *ancestorsVC;
|
||||
|
||||
// seeking for the root presentation VC.
|
||||
if (self.splitViewController) {
|
||||
ancestorsVC = self.splitViewController;
|
||||
} else if (self.navigationController) {
|
||||
ancestorsVC = self.navigationController;
|
||||
} else if (self.tabBarController) {
|
||||
ancestorsVC = self.tabBarController;
|
||||
} else {
|
||||
ancestorsVC = self;
|
||||
}
|
||||
|
||||
if ([ancestorsVC.presentationController isMemberOfClass:HWPanModalPresentationController.class]) {
|
||||
return (HWPanModalPresentationController *) ancestorsVC.presentationController;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the short form Y postion
|
||||
|
||||
- Note: If voiceover is on, the `longFormYPos` is returned.
|
||||
We do not support short form when voiceover is on as it would make it difficult for user to navigate.
|
||||
*/
|
||||
- (CGFloat)shortFormYPos {
|
||||
if (UIAccessibilityIsVoiceOverRunning()) {
|
||||
return self.longFormYPos;
|
||||
} else {
|
||||
CGFloat shortFormYPos = [self topMarginFromPanModalHeight:[self shortFormHeight]] + [self topOffset];
|
||||
return MAX(shortFormYPos, self.longFormYPos);
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)mediumFormYPos {
|
||||
if (UIAccessibilityIsVoiceOverRunning()) {
|
||||
return self.longFormYPos;
|
||||
} else {
|
||||
CGFloat mediumFormYPos = [self topMarginFromPanModalHeight:[self mediumFormHeight]] + [self topOffset];
|
||||
return MAX(mediumFormYPos, self.longFormYPos);
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)longFormYPos {
|
||||
return MAX([self topMarginFromPanModalHeight:[self longFormHeight]], [self topMarginFromPanModalHeight:PanModalHeightMake(PanModalHeightTypeMax, 0)]) + [self topOffset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the container view for relative positioning as this view's frame
|
||||
is adjusted in PanModalPresentationController
|
||||
*/
|
||||
- (CGFloat)bottomYPos {
|
||||
if (self.hw_presentedVC.containerView) {
|
||||
return self.hw_presentedVC.containerView.bounds.size.height - [self topOffset];
|
||||
}
|
||||
return self.view.bounds.size.height;
|
||||
}
|
||||
|
||||
- (CGFloat)topMarginFromPanModalHeight:(PanModalHeight)panModalHeight {
|
||||
switch (panModalHeight.heightType) {
|
||||
case PanModalHeightTypeMax:
|
||||
return 0.0f;
|
||||
case PanModalHeightTypeMaxTopInset:
|
||||
return panModalHeight.height;
|
||||
case PanModalHeightTypeContent:
|
||||
return self.bottomYPos - (panModalHeight.height + self.bottomLayoutOffset);
|
||||
case PanModalHeightTypeContentIgnoringSafeArea:
|
||||
return self.bottomYPos - panModalHeight.height;
|
||||
case PanModalHeightTypeIntrinsic:
|
||||
{
|
||||
[self.view layoutIfNeeded];
|
||||
|
||||
CGSize targetSize = CGSizeMake(self.hw_presentedVC.containerView ? self.hw_presentedVC.containerView.bounds.size.width : [UIScreen mainScreen].bounds.size.width, UILayoutFittingCompressedSize.height);
|
||||
CGFloat intrinsicHeight = [self.view systemLayoutSizeFittingSize:targetSize].height;
|
||||
return self.bottomYPos - (intrinsicHeight + self.bottomLayoutOffset);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
18
Pods/HWPanModal/Sources/Presentable/UIViewController+PanModalDefault.h
generated
Normal file
18
Pods/HWPanModal/Sources/Presentable/UIViewController+PanModalDefault.h
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// UIViewController+PanModalDefault.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
#import <HWPanModal/HWPanModalPanGestureDelegate.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIViewController (PanModalDefault) <HWPanModalPresentable, HWPanModalPanGestureDelegate>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
253
Pods/HWPanModal/Sources/Presentable/UIViewController+PanModalDefault.m
generated
Normal file
253
Pods/HWPanModal/Sources/Presentable/UIViewController+PanModalDefault.m
generated
Normal file
@@ -0,0 +1,253 @@
|
||||
//
|
||||
// UIViewController+PanModalDefault.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import "UIViewController+PanModalDefault.h"
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
|
||||
@implementation UIViewController (PanModalDefault)
|
||||
|
||||
- (UIScrollView *)panScrollable {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGFloat)topOffset {
|
||||
return self.topLayoutOffset + 21.f;
|
||||
}
|
||||
|
||||
- (PanModalHeight)shortFormHeight {
|
||||
return [self longFormHeight];
|
||||
}
|
||||
|
||||
- (PanModalHeight)mediumFormHeight {
|
||||
return [self longFormHeight];
|
||||
}
|
||||
|
||||
- (PanModalHeight)longFormHeight {
|
||||
if ([self panScrollable]) {
|
||||
[[self panScrollable] layoutIfNeeded];
|
||||
return PanModalHeightMake(PanModalHeightTypeContent, MAX([self panScrollable].contentSize.height, [self panScrollable].bounds.size.height));
|
||||
} else {
|
||||
return PanModalHeightMake(PanModalHeightTypeMax, 0);
|
||||
}
|
||||
}
|
||||
|
||||
- (PresentationState)originPresentationState {
|
||||
return PresentationStateShort;
|
||||
}
|
||||
|
||||
- (CGFloat)springDamping {
|
||||
return 0.8;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)transitionDuration {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)dismissalDuration {
|
||||
return [self transitionDuration];
|
||||
}
|
||||
|
||||
- (UIViewAnimationOptions)transitionAnimationOptions {
|
||||
return UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState;
|
||||
}
|
||||
|
||||
- (CGFloat)backgroundAlpha {
|
||||
return 0.7;
|
||||
}
|
||||
|
||||
- (CGFloat)backgroundBlurRadius {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (nonnull UIColor *)backgroundBlurColor {
|
||||
return [UIColor whiteColor];
|
||||
}
|
||||
|
||||
- (HWBackgroundConfig *)backgroundConfig {
|
||||
return [HWBackgroundConfig configWithBehavior:HWBackgroundBehaviorDefault];
|
||||
}
|
||||
|
||||
- (UIEdgeInsets)scrollIndicatorInsets {
|
||||
CGFloat top = [self shouldRoundTopCorners] ? [self cornerRadius] : 0;
|
||||
return UIEdgeInsetsMake(top, 0, self.bottomLayoutOffset, 0);
|
||||
}
|
||||
|
||||
- (BOOL)showsScrollableVerticalScrollIndicator {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutoSetPanScrollContentInset {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)anchorModalToLongForm {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)allowsExtendedPanScrolling {
|
||||
if ([self panScrollable]) {
|
||||
UIScrollView *scrollable = [self panScrollable];
|
||||
|
||||
/*
|
||||
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window.
|
||||
*/
|
||||
if (!scrollable.superview || !scrollable.window) return NO;
|
||||
|
||||
[scrollable layoutIfNeeded];
|
||||
return scrollable.contentSize.height > (scrollable.frame.size.height - self.bottomLayoutOffset);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)allowsDragToDismiss {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)minVerticalVelocityToTriggerDismiss {
|
||||
return 300;
|
||||
}
|
||||
|
||||
- (BOOL)allowsTapBackgroundToDismiss {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)allowScreenEdgeInteractive {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (CGFloat)maxAllowedDistanceToLeftScreenEdgeForPanInteraction {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (CGFloat)minHorizontalVelocityToTriggerScreenEdgeDismiss {
|
||||
return 500;
|
||||
}
|
||||
|
||||
- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle {
|
||||
return PresentingViewControllerAnimationStyleNone;
|
||||
}
|
||||
|
||||
- (BOOL)shouldEnableAppearanceTransition {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAnimatePresentingVC {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (id <HWPresentingViewControllerAnimatedTransitioning>)customPresentingVCAnimation {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isPanScrollEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isUserInteractionEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)allowsPullDownWhenShortState {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isHapticFeedbackEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)allowsTouchEventsPassingThroughTransitionView {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldRoundTopCorners {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)cornerRadius {
|
||||
return 8;
|
||||
}
|
||||
|
||||
- (HWPanModalShadow *)contentShadow {
|
||||
return [HWPanModalShadow panModalShadowNil];
|
||||
}
|
||||
|
||||
- (BOOL)showDragIndicator {
|
||||
if ([self allowsTouchEventsPassingThroughTransitionView]) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable UIView <HWPanModalIndicatorProtocol> *)customIndicatorView {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isAutoHandleKeyboardEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)keyboardOffsetFromInputView {
|
||||
return 5;
|
||||
}
|
||||
|
||||
- (BOOL)shouldRespondToPanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)willRespondToPanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
}
|
||||
|
||||
- (void)didRespondToPanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
}
|
||||
|
||||
- (void)didEndRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
}
|
||||
|
||||
- (BOOL)shouldPrioritizePanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldTransitionToState:(PresentationState)state {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)willTransitionToState:(PresentationState)state {
|
||||
}
|
||||
|
||||
- (void)didChangeTransitionToState:(PresentationState)state {
|
||||
}
|
||||
|
||||
- (void)panModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer dismissPercent:(CGFloat)percent {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalWillDismiss {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalDidDismissed {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalTransitionWillBegin {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalTransitionDidFinish {
|
||||
|
||||
}
|
||||
|
||||
- (void)presentedViewDidMoveToSuperView {
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
21
Pods/HWPanModal/Sources/Presentable/UIViewController+Presentation.h
generated
Normal file
21
Pods/HWPanModal/Sources/Presentable/UIViewController+Presentation.h
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// UIViewController+Presentation.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
#import <HWPanModal/HWPanModalPresentationUpdateProtocol.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* The presented Controller can use the category to update UIPresentationController container.
|
||||
*/
|
||||
@interface UIViewController (Presentation) <HWPanModalPresentationUpdateProtocol>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
70
Pods/HWPanModal/Sources/Presentable/UIViewController+Presentation.m
generated
Normal file
70
Pods/HWPanModal/Sources/Presentable/UIViewController+Presentation.m
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// UIViewController+Presentation.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import "UIViewController+Presentation.h"
|
||||
#import "UIViewController+LayoutHelper.h"
|
||||
#import "HWPanModalPresentationController.h"
|
||||
|
||||
@interface UIViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIViewController (Presentation)
|
||||
|
||||
- (void)hw_panModalTransitionTo:(PresentationState)state {
|
||||
if (!self.hw_presentedVC) return;
|
||||
[self.hw_presentedVC transitionToState:state animated:YES];
|
||||
}
|
||||
|
||||
- (void)hw_panModalTransitionTo:(PresentationState)state animated:(BOOL)animated {
|
||||
if (!self.hw_presentedVC) return;
|
||||
[self.hw_presentedVC transitionToState:state animated:animated];
|
||||
}
|
||||
|
||||
- (void)hw_panModalSetContentOffset:(CGPoint)offset animated:(BOOL)animated {
|
||||
if (!self.hw_presentedVC) return;
|
||||
[self.hw_presentedVC setScrollableContentOffset:offset animated:animated];
|
||||
}
|
||||
|
||||
|
||||
- (void)hw_panModalSetContentOffset:(CGPoint)offset {
|
||||
if (!self.hw_presentedVC) return;
|
||||
[self.hw_presentedVC setScrollableContentOffset:offset animated:YES];
|
||||
}
|
||||
|
||||
- (void)hw_panModalSetNeedsLayoutUpdate {
|
||||
if (!self.hw_presentedVC) return;
|
||||
[self.hw_presentedVC setNeedsLayoutUpdate];
|
||||
}
|
||||
|
||||
- (void)hw_panModalUpdateUserHitBehavior {
|
||||
if (!self.hw_presentedVC) return;
|
||||
[self.hw_presentedVC updateUserHitBehavior];
|
||||
}
|
||||
|
||||
- (void)hw_dismissAnimated:(BOOL)animated completion:(void (^)(void))completion{
|
||||
if (!self.hw_presentedVC) return;
|
||||
[self.hw_presentedVC dismissAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (HWDimmedView *)hw_dimmedView {
|
||||
return self.hw_presentedVC.backgroundView;
|
||||
}
|
||||
|
||||
- (UIView *)hw_rootContainerView {
|
||||
return self.hw_presentedVC.containerView;
|
||||
}
|
||||
|
||||
- (UIView *)hw_contentView {
|
||||
return self.hw_presentedVC.presentedView;
|
||||
}
|
||||
|
||||
- (PresentationState)hw_presentationState {
|
||||
return self.hw_presentedVC.currentPresentationState;
|
||||
}
|
||||
|
||||
@end
|
||||
47
Pods/HWPanModal/Sources/Presenter/HWPanModalPresenterProtocol.h
generated
Normal file
47
Pods/HWPanModal/Sources/Presenter/HWPanModalPresenterProtocol.h
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// HWPanModalPresenterProtocol.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
@class HWPanModalPresentationDelegate;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol HWPanModalPresenter <NSObject>
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL isPanModalPresented;
|
||||
/**
|
||||
* 这里我们将实现UIViewControllerTransitioningDelegate协议的delegate通过runtime存入到viewControllerToPresent中。
|
||||
* use runtime to store this prop to hw_presentedVC
|
||||
*/
|
||||
@property (nonnull, nonatomic, strong) HWPanModalPresentationDelegate *hw_panModalPresentationDelegate;
|
||||
|
||||
/**
|
||||
* Note: This method ONLY for iPad, like UIPopoverPresentationController.
|
||||
*/
|
||||
- (void)presentPanModal:(UIViewController<HWPanModalPresentable> *)viewControllerToPresent
|
||||
sourceView:(nullable UIView *)sourceView
|
||||
sourceRect:(CGRect)rect;
|
||||
|
||||
- (void)presentPanModal:(UIViewController<HWPanModalPresentable> *)viewControllerToPresent
|
||||
sourceView:(nullable UIView *)sourceView
|
||||
sourceRect:(CGRect)rect
|
||||
completion:(void (^ __nullable)(void))completion;
|
||||
|
||||
/**
|
||||
* Present the Controller from bottom.
|
||||
*/
|
||||
- (void)presentPanModal:(UIViewController<HWPanModalPresentable> *)viewControllerToPresent;
|
||||
|
||||
- (void)presentPanModal:(UIViewController<HWPanModalPresentable> *)viewControllerToPresent
|
||||
completion:(void (^ __nullable)(void))completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
17
Pods/HWPanModal/Sources/Presenter/UIViewController+PanModalPresenter.h
generated
Normal file
17
Pods/HWPanModal/Sources/Presenter/UIViewController+PanModalPresenter.h
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// UIViewController+PanModalPresenter.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "HWPanModalPresenterProtocol.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIViewController (PanModalPresenter) <HWPanModalPresenter>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
64
Pods/HWPanModal/Sources/Presenter/UIViewController+PanModalPresenter.m
generated
Normal file
64
Pods/HWPanModal/Sources/Presenter/UIViewController+PanModalPresenter.m
generated
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// UIViewController+PanModalPresenter.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/29.
|
||||
//
|
||||
|
||||
#import "UIViewController+PanModalPresenter.h"
|
||||
#import "HWPanModalPresentationDelegate.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation UIViewController (PanModalPresenter)
|
||||
|
||||
- (BOOL)isPanModalPresented {
|
||||
return [self.transitioningDelegate isKindOfClass:HWPanModalPresentationDelegate.class];
|
||||
}
|
||||
|
||||
- (void)presentPanModal:(UIViewController<HWPanModalPresentable> *)viewControllerToPresent sourceView:(UIView *)sourceView sourceRect:(CGRect)rect completion:(void (^)(void))completion {
|
||||
|
||||
HWPanModalPresentationDelegate *delegate = [HWPanModalPresentationDelegate new];
|
||||
viewControllerToPresent.hw_panModalPresentationDelegate = delegate;
|
||||
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad &&
|
||||
(sourceView && !CGRectEqualToRect(rect, CGRectZero))) {
|
||||
viewControllerToPresent.modalPresentationStyle = UIModalPresentationPopover;
|
||||
viewControllerToPresent.popoverPresentationController.sourceRect = rect;
|
||||
viewControllerToPresent.popoverPresentationController.sourceView = sourceView;
|
||||
viewControllerToPresent.popoverPresentationController.delegate = delegate;
|
||||
} else {
|
||||
|
||||
viewControllerToPresent.modalPresentationStyle = UIModalPresentationCustom;
|
||||
viewControllerToPresent.modalPresentationCapturesStatusBarAppearance = YES;
|
||||
viewControllerToPresent.transitioningDelegate = delegate;
|
||||
}
|
||||
|
||||
// fix for iOS 8 issue: the present action will delay.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self presentViewController:viewControllerToPresent animated:YES completion:completion];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)presentPanModal:(UIViewController <HWPanModalPresentable> *)viewControllerToPresent sourceView:(nullable UIView *)sourceView sourceRect:(CGRect)rect {
|
||||
[self presentPanModal:viewControllerToPresent sourceView:sourceView sourceRect:rect completion:nil];
|
||||
|
||||
}
|
||||
|
||||
- (void)presentPanModal:(UIViewController <HWPanModalPresentable> *)viewControllerToPresent {
|
||||
[self presentPanModal:viewControllerToPresent sourceView:nil sourceRect:CGRectZero];
|
||||
}
|
||||
|
||||
- (void)presentPanModal:(UIViewController<HWPanModalPresentable> *)viewControllerToPresent completion:(void (^)(void))completion {
|
||||
[self presentPanModal:viewControllerToPresent sourceView:nil sourceRect:CGRectZero completion:completion];
|
||||
}
|
||||
|
||||
- (HWPanModalPresentationDelegate *)hw_panModalPresentationDelegate {
|
||||
return objc_getAssociatedObject(self, _cmd);
|
||||
}
|
||||
|
||||
- (void)setHw_panModalPresentationDelegate:(HWPanModalPresentationDelegate *)hw_panModalPresentationDelegate {
|
||||
objc_setAssociatedObject(self, @selector(hw_panModalPresentationDelegate), hw_panModalPresentationDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
37
Pods/HWPanModal/Sources/View/HWBackgroundConfig.h
generated
Normal file
37
Pods/HWPanModal/Sources/View/HWBackgroundConfig.h
generated
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// HWBackgroundConfig.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2020/4/17.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, HWBackgroundBehavior) {
|
||||
HWBackgroundBehaviorDefault, // use background alpha
|
||||
HWBackgroundBehaviorSystemVisualEffect, // use system UIVisualEffect object
|
||||
HWBackgroundBehaviorCustomBlurEffect, // use custom blur
|
||||
};
|
||||
|
||||
@interface HWBackgroundConfig : NSObject
|
||||
|
||||
@property (nonatomic, assign) HWBackgroundBehavior backgroundBehavior;
|
||||
// ONLY works for backgroundBehavior = HWBackgroundBehaviorDefault
|
||||
@property (nonatomic, assign) CGFloat backgroundAlpha; // default is 0.7
|
||||
// ONLY works for backgroundBehavior = HWBackgroundBehaviorSystemVisualEffect
|
||||
@property (nonatomic, strong) UIVisualEffect *visualEffect; // default is UIBlurEffectStyleLight
|
||||
|
||||
// ONLY works for backgroundBehavior = HWBackgroundBehaviorCustomBlurEffect
|
||||
@property (nonatomic, strong) UIColor *blurTintColor; // default is white color
|
||||
@property (nonatomic, assign) CGFloat backgroundBlurRadius; // default is 10
|
||||
|
||||
- (instancetype)initWithBehavior:(HWBackgroundBehavior)backgroundBehavior;
|
||||
|
||||
+ (instancetype)configWithBehavior:(HWBackgroundBehavior)backgroundBehavior;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
57
Pods/HWPanModal/Sources/View/HWBackgroundConfig.m
generated
Normal file
57
Pods/HWPanModal/Sources/View/HWBackgroundConfig.m
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// HWBackgroundConfig.m
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2020/4/17.
|
||||
//
|
||||
|
||||
#import "HWBackgroundConfig.h"
|
||||
|
||||
@implementation HWBackgroundConfig
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.backgroundBehavior = HWBackgroundBehaviorDefault;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBehavior:(HWBackgroundBehavior)backgroundBehavior {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.backgroundBehavior = backgroundBehavior;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)configWithBehavior:(HWBackgroundBehavior)backgroundBehavior {
|
||||
return [[self alloc] initWithBehavior:backgroundBehavior];
|
||||
}
|
||||
|
||||
#pragma mark - Setter
|
||||
|
||||
- (void)setBackgroundBehavior:(HWBackgroundBehavior)backgroundBehavior {
|
||||
_backgroundBehavior = backgroundBehavior;
|
||||
|
||||
switch (backgroundBehavior) {
|
||||
case HWBackgroundBehaviorSystemVisualEffect: {
|
||||
self.visualEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
}
|
||||
break;
|
||||
case HWBackgroundBehaviorCustomBlurEffect: {
|
||||
self.backgroundBlurRadius = 10;
|
||||
self.blurTintColor = [UIColor whiteColor];
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
self.backgroundAlpha = 0.7;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
41
Pods/HWPanModal/Sources/View/HWDimmedView.h
generated
Normal file
41
Pods/HWPanModal/Sources/View/HWDimmedView.h
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// HWDimmedView.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@class HWBackgroundConfig;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, DimState) {
|
||||
DimStateMax,
|
||||
DimStateOff,
|
||||
DimStatePercent,
|
||||
};
|
||||
|
||||
typedef void(^didTap)(UITapGestureRecognizer *recognizer);
|
||||
|
||||
@interface HWDimmedView : UIView
|
||||
|
||||
@property (nonatomic, assign) DimState dimState;
|
||||
@property (nonatomic, assign) CGFloat percent;
|
||||
@property (nullable, nonatomic, copy) didTap tapBlock;
|
||||
@property (nullable, nonatomic, strong) UIColor *blurTintColor;
|
||||
|
||||
@property (nonatomic, readonly) HWBackgroundConfig *backgroundConfig;
|
||||
|
||||
/**
|
||||
* init with the max dim alpha & max blur radius.
|
||||
*/
|
||||
- (instancetype)initWithDimAlpha:(CGFloat)dimAlpha blurRadius:(CGFloat)blurRadius;
|
||||
|
||||
- (instancetype)initWithBackgroundConfig:(HWBackgroundConfig *)backgroundConfig;
|
||||
|
||||
- (void)reloadConfig:(HWBackgroundConfig *)backgroundConfig;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
202
Pods/HWPanModal/Sources/View/HWDimmedView.m
generated
Normal file
202
Pods/HWPanModal/Sources/View/HWDimmedView.m
generated
Normal file
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// HWDimmedView.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import "HWDimmedView.h"
|
||||
#import "HWVisualEffectView.h"
|
||||
#import "HWBackgroundConfig.h"
|
||||
|
||||
@interface HWDimmedView ()
|
||||
|
||||
@property (nonatomic, strong) UIView *backgroundView;
|
||||
@property (nonatomic, strong) HWVisualEffectView *blurView;
|
||||
@property (nonatomic, strong) HWBackgroundConfig *backgroundConfig;
|
||||
|
||||
@property (nonatomic, assign) CGFloat maxDimAlpha;
|
||||
@property (nonatomic, assign) CGFloat maxBlurRadius;
|
||||
@property (nonatomic, assign) CGFloat maxBlurTintAlpha;
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *tapGestureRecognizer;
|
||||
@property (nonatomic, assign) BOOL isBlurMode;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWDimmedView
|
||||
|
||||
- (instancetype)initWithDimAlpha:(CGFloat)dimAlpha blurRadius:(CGFloat)blurRadius {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
_maxBlurRadius = blurRadius;
|
||||
_maxDimAlpha = dimAlpha;
|
||||
[self commonInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_maxDimAlpha = 0.7;
|
||||
[self commonInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBackgroundConfig:(HWBackgroundConfig *)backgroundConfig {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
self.backgroundConfig = backgroundConfig;
|
||||
_maxDimAlpha = backgroundConfig.backgroundAlpha;
|
||||
_maxBlurRadius = backgroundConfig.backgroundBlurRadius;
|
||||
_blurTintColor = backgroundConfig.blurTintColor;
|
||||
|
||||
[self commonInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit {
|
||||
_dimState = DimStateOff;
|
||||
_maxBlurTintAlpha = 0.5;
|
||||
// default, max alpha.
|
||||
_percent = 1;
|
||||
_tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapView)];
|
||||
[self addGestureRecognizer:_tapGestureRecognizer];
|
||||
|
||||
[self setupView];
|
||||
}
|
||||
|
||||
- (void)setupView {
|
||||
self.isBlurMode = self.maxBlurRadius > 0 || self.backgroundConfig.visualEffect;
|
||||
if (self.isBlurMode) {
|
||||
[self addSubview:self.blurView];
|
||||
[self configBlurView];
|
||||
} else {
|
||||
[self addSubview:self.backgroundView];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - layout
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
// not call getter.
|
||||
_blurView.frame = self.bounds;
|
||||
_backgroundView.frame = self.bounds;
|
||||
}
|
||||
|
||||
#pragma mark - touch action
|
||||
|
||||
- (void)didTapView {
|
||||
self.tapBlock ? self.tapBlock(self.tapGestureRecognizer) : nil;
|
||||
}
|
||||
|
||||
#pragma mark - public method
|
||||
|
||||
- (void)reloadConfig:(HWBackgroundConfig *)backgroundConfig {
|
||||
|
||||
for (UIView *view in self.subviews) {
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
|
||||
self.backgroundConfig = backgroundConfig;
|
||||
_maxDimAlpha = backgroundConfig.backgroundAlpha;
|
||||
_maxBlurRadius = backgroundConfig.backgroundBlurRadius;
|
||||
_blurTintColor = backgroundConfig.blurTintColor;
|
||||
|
||||
[self setupView];
|
||||
|
||||
DimState state = self.dimState;
|
||||
self.dimState = state;
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (void)updateAlpha {
|
||||
CGFloat alpha = 0;
|
||||
CGFloat blurRadius = 0;
|
||||
CGFloat blurTintAlpha = 0;
|
||||
|
||||
switch (self.dimState) {
|
||||
case DimStateMax:{
|
||||
alpha = self.maxDimAlpha;
|
||||
blurRadius = self.maxBlurRadius;
|
||||
blurTintAlpha = self.maxBlurTintAlpha;
|
||||
}
|
||||
break;
|
||||
case DimStatePercent: {
|
||||
CGFloat percent = MAX(0, MIN(1.0f, self.percent));
|
||||
alpha = self.maxDimAlpha * percent;
|
||||
blurRadius = self.maxBlurRadius * percent;
|
||||
blurTintAlpha = self.maxBlurTintAlpha * percent;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (self.isBlurMode) {
|
||||
if (self.backgroundConfig.visualEffect) return;
|
||||
|
||||
self.blurView.blurRadius = blurRadius;
|
||||
self.blurView.colorTintAlpha = blurTintAlpha;
|
||||
} else {
|
||||
self.backgroundView.alpha = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configBlurView {
|
||||
if (self.backgroundConfig.visualEffect) {
|
||||
[_blurView updateBlurEffect:self.backgroundConfig.visualEffect];
|
||||
} else {
|
||||
_blurView.colorTint = [UIColor whiteColor];
|
||||
_blurView.colorTintAlpha = self.maxBlurTintAlpha;
|
||||
_blurView.userInteractionEnabled = NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Setter
|
||||
|
||||
- (void)setDimState:(DimState)dimState {
|
||||
_dimState = dimState;
|
||||
[self updateAlpha];
|
||||
}
|
||||
|
||||
- (void)setPercent:(CGFloat)percent {
|
||||
_percent = percent;
|
||||
[self updateAlpha];
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (UIView *)backgroundView {
|
||||
if (!_backgroundView) {
|
||||
_backgroundView = [UIView new];
|
||||
_backgroundView.userInteractionEnabled = NO;
|
||||
_backgroundView.alpha = 0;
|
||||
_backgroundView.backgroundColor = [UIColor blackColor];
|
||||
}
|
||||
return _backgroundView;
|
||||
}
|
||||
|
||||
- (HWVisualEffectView *)blurView {
|
||||
if (!_blurView) {
|
||||
_blurView = [HWVisualEffectView new];
|
||||
}
|
||||
return _blurView;
|
||||
}
|
||||
|
||||
#pragma mark - Setter
|
||||
|
||||
- (void)setBlurTintColor:(UIColor *)blurTintColor {
|
||||
_blurTintColor = blurTintColor;
|
||||
_blurView.colorTint = _blurTintColor;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
34
Pods/HWPanModal/Sources/View/HWPanContainerView.h
generated
Normal file
34
Pods/HWPanModal/Sources/View/HWPanContainerView.h
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// HWPanContainerView.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanContainerView : UIView
|
||||
|
||||
/// the presented view should add to the content view.
|
||||
@property (nonatomic, strong, readonly) UIView *contentView;
|
||||
|
||||
- (instancetype)initWithPresentedView:(UIView *)presentedView frame:(CGRect)frame;
|
||||
|
||||
- (void)updateShadow:(UIColor *)shadowColor
|
||||
shadowRadius:(CGFloat)shadowRadius
|
||||
shadowOffset:(CGSize)shadowOffset
|
||||
shadowOpacity:(float)shadowOpacity;
|
||||
|
||||
- (void)clearShadow;
|
||||
|
||||
@end
|
||||
|
||||
@interface UIView (PanContainer)
|
||||
|
||||
@property (nullable, nonatomic, strong, readonly) HWPanContainerView *panContainerView;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
60
Pods/HWPanModal/Sources/View/HWPanContainerView.m
generated
Normal file
60
Pods/HWPanModal/Sources/View/HWPanContainerView.m
generated
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// HWPanContainerView.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/4/26.
|
||||
//
|
||||
|
||||
#import "HWPanContainerView.h"
|
||||
|
||||
@interface HWPanContainerView ()
|
||||
|
||||
@property (nonatomic, strong) UIView *contentView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanContainerView
|
||||
|
||||
- (instancetype)initWithPresentedView:(UIView *)presentedView frame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_contentView = [UIView new];
|
||||
|
||||
_contentView.frame = self.bounds;
|
||||
[self addSubview:_contentView];
|
||||
[_contentView addSubview:presentedView];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateShadow:(UIColor *)shadowColor shadowRadius:(CGFloat)shadowRadius shadowOffset:(CGSize)shadowOffset shadowOpacity:(float)shadowOpacity {
|
||||
|
||||
self.layer.shadowColor = shadowColor.CGColor;
|
||||
self.layer.shadowRadius = shadowRadius;
|
||||
self.layer.shadowOffset = shadowOffset;
|
||||
self.layer.shadowOpacity = shadowOpacity;
|
||||
}
|
||||
|
||||
- (void)clearShadow {
|
||||
self.layer.shadowColor = nil;
|
||||
self.layer.shadowRadius = 3.0;
|
||||
self.layer.shadowOffset = CGSizeZero;
|
||||
self.layer.shadowOpacity = 0.0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIView (PanContainer)
|
||||
|
||||
- (HWPanContainerView *)panContainerView {
|
||||
for (UIView *subview in self.subviews) {
|
||||
if ([subview isKindOfClass:HWPanContainerView.class]) {
|
||||
return (HWPanContainerView *) subview;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
19
Pods/HWPanModal/Sources/View/HWPanIndicatorView.h
generated
Normal file
19
Pods/HWPanModal/Sources/View/HWPanIndicatorView.h
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// HWPanIndicatorView.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/16.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalIndicatorProtocol.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanIndicatorView : UIView <HWPanModalIndicatorProtocol>
|
||||
|
||||
@property (nonnull, nonatomic, strong) UIColor *indicatorColor;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
114
Pods/HWPanModal/Sources/View/HWPanIndicatorView.m
generated
Normal file
114
Pods/HWPanModal/Sources/View/HWPanIndicatorView.m
generated
Normal file
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// HWPanIndicatorView.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/5/16.
|
||||
//
|
||||
|
||||
#import "UIView+HW_Frame.h"
|
||||
#import <HWPanModal/HWPanIndicatorView.h>
|
||||
|
||||
@interface HWPanIndicatorView ()
|
||||
|
||||
@property (nonatomic, strong) UIView *leftView;
|
||||
@property (nonatomic, strong) UIView *rightView;
|
||||
|
||||
@property (nonatomic, assign) HWIndicatorState state;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanIndicatorView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
[self addSubview:self.leftView];
|
||||
[self addSubview:self.rightView];
|
||||
self.indicatorColor = [UIColor colorWithRed:0.792 green:0.788 blue:0.812 alpha:1.00];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)animate:(void (^)(void))animations {
|
||||
[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:1 initialSpringVelocity:1 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut animations:animations completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalIndicatorProtocol
|
||||
|
||||
- (void)didChangeToState:(HWIndicatorState)state {
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
- (CGSize)indicatorSize {
|
||||
return CGSizeMake(34, 13);
|
||||
}
|
||||
|
||||
- (void)setupSubviews {
|
||||
CGSize size = [self indicatorSize];
|
||||
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, size.width, size.height);
|
||||
CGFloat height = 5;
|
||||
CGFloat correction = height / 2;
|
||||
|
||||
self.leftView.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame) / 2 + correction, height);
|
||||
self.leftView.hw_centerY = self.hw_height / 2;
|
||||
self.leftView.layer.cornerRadius = MIN(self.leftView.hw_width, self.leftView.hw_height) / 2;
|
||||
|
||||
self.rightView.frame = CGRectMake(CGRectGetWidth(self.frame) / 2 - correction, 0, CGRectGetWidth(self.frame) / 2 + correction, height);
|
||||
self.rightView.hw_centerY = self.hw_height / 2;
|
||||
self.rightView.layer.cornerRadius = MIN(self.rightView.hw_width, self.rightView.hw_height) / 2;
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (UIView *)leftView {
|
||||
if (!_leftView) {
|
||||
_leftView = [UIView new];
|
||||
}
|
||||
return _leftView;
|
||||
}
|
||||
|
||||
- (UIView *)rightView {
|
||||
if (!_rightView) {
|
||||
_rightView = [UIView new];
|
||||
}
|
||||
return _rightView;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setter
|
||||
|
||||
- (void)setIndicatorColor:(UIColor *)indicatorColor {
|
||||
_indicatorColor = indicatorColor;
|
||||
self.leftView.backgroundColor = indicatorColor;
|
||||
self.rightView.backgroundColor = indicatorColor;
|
||||
}
|
||||
|
||||
- (void)setState:(HWIndicatorState)state {
|
||||
|
||||
_state = state;
|
||||
|
||||
switch (state) {
|
||||
case HWIndicatorStateNormal: {
|
||||
CGFloat angle = 20 * M_PI / 180;
|
||||
[self animate:^{
|
||||
self.leftView.transform = CGAffineTransformMakeRotation(angle);
|
||||
self.rightView.transform = CGAffineTransformMakeRotation(-angle);
|
||||
}];
|
||||
}
|
||||
break;
|
||||
case HWIndicatorStatePullDown: {
|
||||
[self animate:^{
|
||||
self.leftView.transform = CGAffineTransformIdentity;
|
||||
self.rightView.transform = CGAffineTransformIdentity;
|
||||
}];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
36
Pods/HWPanModal/Sources/View/HWPanModalIndicatorProtocol.h
generated
Normal file
36
Pods/HWPanModal/Sources/View/HWPanModalIndicatorProtocol.h
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// HWPanModalIndicatorProtocol.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/8/9.
|
||||
//
|
||||
|
||||
typedef NS_ENUM(NSUInteger, HWIndicatorState) {
|
||||
HWIndicatorStateNormal NS_SWIFT_NAME(normal), // origin state
|
||||
HWIndicatorStatePullDown NS_SWIFT_NAME(pull), // drag down state
|
||||
};
|
||||
|
||||
static CGFloat const kIndicatorYOffset = 5;
|
||||
|
||||
@protocol HWPanModalIndicatorProtocol <NSObject>
|
||||
|
||||
/**
|
||||
* When user drags, the state will change.
|
||||
* You can change your UI here.
|
||||
* @param state The state when drag changed.
|
||||
*/
|
||||
- (void)didChangeToState:(HWIndicatorState)state;
|
||||
|
||||
/**
|
||||
* Tell the size of the indicator.
|
||||
*/
|
||||
- (CGSize)indicatorSize;
|
||||
|
||||
/**
|
||||
* You can layout your UI here if you need.
|
||||
* This method called when indicator added to super view
|
||||
*/
|
||||
- (void)setupSubviews;
|
||||
|
||||
@end
|
||||
|
||||
26
Pods/HWPanModal/Sources/View/HWPanModalShadow.h
generated
Normal file
26
Pods/HWPanModal/Sources/View/HWPanModalShadow.h
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// HWPanModalShadow.h
|
||||
// Pods
|
||||
//
|
||||
// Created by hb on 2023/8/3.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanModalShadow : NSObject
|
||||
|
||||
@property (nonatomic, strong) UIColor *shadowColor;
|
||||
@property (nonatomic, assign) CGFloat shadowRadius;
|
||||
@property (nonatomic, assign) CGSize shadowOffset;
|
||||
@property (nonatomic, assign) CGFloat shadowOpacity;
|
||||
|
||||
- (instancetype)initWithColor:(UIColor *)shadowColor shadowRadius:(CGFloat)shadowRadius shadowOffset:(CGSize)shadowOffset shadowOpacity:(CGFloat)shadowOpacity;
|
||||
|
||||
+ (instancetype)panModalShadowNil;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
28
Pods/HWPanModal/Sources/View/HWPanModalShadow.m
generated
Normal file
28
Pods/HWPanModal/Sources/View/HWPanModalShadow.m
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// HWPanModalShadow.m
|
||||
// Pods
|
||||
//
|
||||
// Created by hb on 2023/8/3.
|
||||
//
|
||||
|
||||
#import "HWPanModalShadow.h"
|
||||
|
||||
@implementation HWPanModalShadow
|
||||
|
||||
- (instancetype)initWithColor:(UIColor *)shadowColor shadowRadius:(CGFloat)shadowRadius shadowOffset:(CGSize)shadowOffset shadowOpacity:(CGFloat)shadowOpacity {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_shadowColor = shadowColor;
|
||||
_shadowRadius = shadowRadius;
|
||||
_shadowOffset = shadowOffset;
|
||||
_shadowOpacity = shadowOpacity;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)panModalShadowNil {
|
||||
return [[HWPanModalShadow alloc] initWithColor:[UIColor clearColor] shadowRadius:0 shadowOffset:CGSizeZero shadowOpacity:0];
|
||||
}
|
||||
|
||||
@end
|
||||
39
Pods/HWPanModal/Sources/View/HWVisualEffectView.h
generated
Normal file
39
Pods/HWPanModal/Sources/View/HWVisualEffectView.h
generated
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// HWVisualEffectView.h
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/6/14.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWVisualEffectView : UIVisualEffectView
|
||||
|
||||
/**
|
||||
* tint color
|
||||
* default is nil
|
||||
*/
|
||||
@property (nullable, nonatomic, strong) UIColor *colorTint;
|
||||
/**
|
||||
* tint color alpha
|
||||
* default is 0.0
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat colorTintAlpha;
|
||||
/**
|
||||
* blur radius, change it to make blur affect
|
||||
* default is 0.0
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat blurRadius;
|
||||
/**
|
||||
* scale factor.
|
||||
* default is 1.0
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat scale;
|
||||
|
||||
- (void)updateBlurEffect:(UIVisualEffect *)effect;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
111
Pods/HWPanModal/Sources/View/HWVisualEffectView.m
generated
Normal file
111
Pods/HWPanModal/Sources/View/HWVisualEffectView.m
generated
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// HWVisualEffectView.m
|
||||
// HWPanModal
|
||||
//
|
||||
// Created by heath wang on 2019/6/14.
|
||||
//
|
||||
|
||||
#import "HWVisualEffectView.h"
|
||||
|
||||
NSString * const kInternalCustomBlurEffect = @"_UICustomBlurEffect";
|
||||
NSString * const kHWBlurEffectColorTintKey = @"colorTint";
|
||||
NSString * const kHWBlurEffectColorTintAlphaKey = @"colorTintAlpha";
|
||||
NSString * const kHWBlurEffectBlurRadiusKey = @"blurRadius";
|
||||
NSString * const kHWBlurEffectScaleKey = @"scale";
|
||||
|
||||
@interface HWVisualEffectView ()
|
||||
|
||||
@property (nonatomic, strong) UIVisualEffect *blurEffect;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWVisualEffectView
|
||||
|
||||
@synthesize colorTint = _colorTint;
|
||||
@synthesize colorTintAlpha = _colorTintAlpha;
|
||||
@synthesize blurRadius = _blurRadius;
|
||||
@synthesize scale = _scale;
|
||||
|
||||
#pragma mark - init
|
||||
|
||||
- (instancetype)initWithEffect:(UIVisualEffect *)effect {
|
||||
self = [super initWithEffect:effect];
|
||||
if (self) {
|
||||
self.scale = 1;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - public method
|
||||
|
||||
- (void)updateBlurEffect:(UIVisualEffect *)effect {
|
||||
self.blurEffect = effect;
|
||||
self.effect = self.blurEffect;
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (nullable id)__valueForKey:(NSString *)key {
|
||||
if (![NSStringFromClass(self.blurEffect.class) isEqualToString:kInternalCustomBlurEffect]) {
|
||||
return @(0);
|
||||
}
|
||||
return [self.blurEffect valueForKey:key];
|
||||
}
|
||||
|
||||
- (void)__setValue:(id)value forKey:(NSString *)key {
|
||||
if (![NSStringFromClass(self.blurEffect.class) isEqualToString:kInternalCustomBlurEffect]) {
|
||||
self.effect = self.blurEffect;
|
||||
return;
|
||||
}
|
||||
[self.blurEffect setValue:value forKey:key];
|
||||
self.effect = self.blurEffect;
|
||||
}
|
||||
|
||||
#pragma mark - Getter & Setter
|
||||
|
||||
- (UIVisualEffect *)blurEffect {
|
||||
if (!_blurEffect) {
|
||||
if (NSClassFromString(kInternalCustomBlurEffect)) {
|
||||
_blurEffect = (UIBlurEffect *)[NSClassFromString(@"_UICustomBlurEffect") new];
|
||||
} else {
|
||||
_blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
}
|
||||
}
|
||||
|
||||
return _blurEffect;
|
||||
}
|
||||
|
||||
- (UIColor *)colorTint {
|
||||
return [self __valueForKey:kHWBlurEffectColorTintKey];
|
||||
}
|
||||
|
||||
- (void)setColorTint:(UIColor *)colorTint {
|
||||
[self __setValue:colorTint forKey:kHWBlurEffectColorTintKey];
|
||||
}
|
||||
|
||||
- (CGFloat)colorTintAlpha {
|
||||
return ((NSNumber *)[self __valueForKey:kHWBlurEffectColorTintAlphaKey]).floatValue;
|
||||
}
|
||||
|
||||
- (void)setColorTintAlpha:(CGFloat)colorTintAlpha {
|
||||
[self __setValue:@(colorTintAlpha) forKey:kHWBlurEffectColorTintAlphaKey];
|
||||
}
|
||||
|
||||
- (CGFloat)blurRadius {
|
||||
return ((NSNumber *)[self __valueForKey:kHWBlurEffectBlurRadiusKey]).floatValue;
|
||||
}
|
||||
|
||||
- (void)setBlurRadius:(CGFloat)blurRadius {
|
||||
[self __setValue:@(blurRadius) forKey:kHWBlurEffectBlurRadiusKey];
|
||||
}
|
||||
|
||||
- (CGFloat)scale {
|
||||
return ((NSNumber *)[self __valueForKey:kHWBlurEffectScaleKey]).floatValue;
|
||||
}
|
||||
|
||||
- (void)setScale:(CGFloat)scale {
|
||||
[self __setValue:@(scale) forKey:kHWBlurEffectScaleKey];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
39
Pods/HWPanModal/Sources/View/PanModal/HWPanModalContainerView.h
generated
Normal file
39
Pods/HWPanModal/Sources/View/PanModal/HWPanModalContainerView.h
generated
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// HWPanModalContainerView.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/17.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
|
||||
@class HWPanModalContentView;
|
||||
@class HWDimmedView;
|
||||
@class HWPanContainerView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HWPanModalContainerView : UIView
|
||||
|
||||
@property (nonatomic, readonly) HWDimmedView *backgroundView;
|
||||
@property (readonly) HWPanContainerView *panContainerView;
|
||||
@property (nonatomic, readonly) PresentationState currentPresentationState;
|
||||
|
||||
- (instancetype)initWithPresentingView:(UIView *)presentingView contentView:(HWPanModalContentView<HWPanModalPresentable> *)contentView;
|
||||
|
||||
- (void)show;
|
||||
|
||||
- (void)dismissAnimated:(BOOL)flag completion:(void (^)(void))completion;
|
||||
|
||||
- (void)setNeedsLayoutUpdate;
|
||||
|
||||
- (void)updateUserHitBehavior;
|
||||
|
||||
- (void)transitionToState:(PresentationState)state animated:(BOOL)animated;
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
546
Pods/HWPanModal/Sources/View/PanModal/HWPanModalContainerView.m
generated
Normal file
546
Pods/HWPanModal/Sources/View/PanModal/HWPanModalContainerView.m
generated
Normal file
@@ -0,0 +1,546 @@
|
||||
//
|
||||
// HWPanModalContainerView.m
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/17.
|
||||
//
|
||||
|
||||
#import "HWPanModalContainerView.h"
|
||||
#import "HWPanModalContentView.h"
|
||||
#import "HWPanModalPresentableHandler.h"
|
||||
#import "HWDimmedView.h"
|
||||
#import "HWPanContainerView.h"
|
||||
#import "UIView+HW_Frame.h"
|
||||
#import "HWPanIndicatorView.h"
|
||||
#import "HWPanModalAnimator.h"
|
||||
|
||||
@interface HWPanModalContainerView () <HWPanModalPresentableHandlerDelegate, HWPanModalPresentableHandlerDataSource>
|
||||
|
||||
@property (nonatomic, strong) HWPanModalContentView<HWPanModalPresentable> *contentView;
|
||||
@property (nonatomic, weak) UIView *presentingView;
|
||||
|
||||
@property (nonatomic, strong) HWPanModalPresentableHandler *handler;
|
||||
|
||||
// 判断弹出的view是否在做动画
|
||||
@property (nonatomic, assign) BOOL isPresentedViewAnimating;
|
||||
@property (nonatomic, assign) PresentationState currentPresentationState;
|
||||
@property (nonatomic, assign) BOOL isPresenting;
|
||||
@property (nonatomic, assign) BOOL isDismissing;
|
||||
|
||||
// view
|
||||
@property (nonatomic, strong) HWDimmedView *backgroundView;
|
||||
@property (nonatomic, strong) HWPanContainerView *panContainerView;
|
||||
@property (nonatomic, strong) UIView<HWPanModalIndicatorProtocol> *dragIndicatorView;
|
||||
|
||||
@property (nonatomic, copy) void(^animationBlock)(void);
|
||||
|
||||
@property (nullable, nonatomic, strong) UISelectionFeedbackGenerator *feedbackGenerator API_AVAILABLE(ios(10.0));
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalContainerView
|
||||
|
||||
- (instancetype)initWithPresentingView:(UIView *)presentingView contentView:(HWPanModalContentView<HWPanModalPresentable> *)contentView {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_presentingView = presentingView;
|
||||
_contentView = contentView;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)show {
|
||||
[self prepare];
|
||||
[self presentAnimationWillBegin];
|
||||
[self beginPresentAnimation];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(BOOL)flag completion:(void (^)(void))completion {
|
||||
if (flag) {
|
||||
self.animationBlock = completion;
|
||||
[self dismiss:NO mode:PanModalInteractiveModeNone];
|
||||
} else {
|
||||
self.isDismissing = YES;
|
||||
[[self presentable] panModalWillDismiss];
|
||||
[self removeFromSuperview];
|
||||
[[self presentable] panModalDidDismissed];
|
||||
|
||||
completion ? completion() : nil;
|
||||
self.isDismissing = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)prepare {
|
||||
[self.presentingView addSubview:self];
|
||||
self.frame = self.presentingView.bounds;
|
||||
|
||||
_handler = [[HWPanModalPresentableHandler alloc] initWithPresentable:self.contentView];
|
||||
_handler.delegate = self;
|
||||
_handler.dataSource = self;
|
||||
|
||||
if (@available(iOS 10.0, *)) {
|
||||
_feedbackGenerator = [UISelectionFeedbackGenerator new];
|
||||
[_feedbackGenerator prepare];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
|
||||
- (void)willMoveToSuperview:(UIView *)newSuperview {
|
||||
[super willMoveToSuperview:newSuperview];
|
||||
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
||||
[self.superview removeObserver:self forKeyPath:@"frame"];
|
||||
[newSuperview addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
||||
if (object == self.presentingView && [keyPath isEqualToString:@"frame"]) {
|
||||
self.frame = self.presentingView.bounds;
|
||||
[self setNeedsLayoutUpdate];
|
||||
[self updateDragIndicatorViewFrame];
|
||||
[self.contentView hw_panModalTransitionTo:self.contentView.hw_presentationState animated:NO];
|
||||
} else {
|
||||
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)presentAnimationWillBegin {
|
||||
[[self presentable] panModalTransitionWillBegin];
|
||||
[self layoutBackgroundView];
|
||||
|
||||
if ([[self presentable] originPresentationState] == PresentationStateLong) {
|
||||
self.currentPresentationState = PresentationStateLong;
|
||||
} else if ([[self presentable] originPresentationState] == PresentationStateMedium) {
|
||||
self.currentPresentationState = PresentationStateMedium;
|
||||
}
|
||||
|
||||
[self addSubview:self.panContainerView];
|
||||
[self layoutPresentedView];
|
||||
|
||||
[self.handler configureScrollViewInsets];
|
||||
[[self presentable] presentedViewDidMoveToSuperView];
|
||||
}
|
||||
|
||||
- (void)beginPresentAnimation {
|
||||
self.isPresenting = YES;
|
||||
CGFloat yPos = self.contentView.shortFormYPos;
|
||||
if ([[self presentable] originPresentationState] == PresentationStateLong) {
|
||||
yPos = self.contentView.longFormYPos;
|
||||
} else if ([[self presentable] originPresentationState] == PresentationStateMedium) {
|
||||
yPos = self.contentView.mediumFormYPos;
|
||||
}
|
||||
|
||||
// refresh layout
|
||||
[self configureViewLayout];
|
||||
[self adjustPresentedViewFrame];
|
||||
|
||||
self.panContainerView.hw_top = self.hw_height;
|
||||
|
||||
if ([[self presentable] isHapticFeedbackEnabled]) {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
[self.feedbackGenerator selectionChanged];
|
||||
}
|
||||
}
|
||||
|
||||
[HWPanModalAnimator animate:^{
|
||||
self.panContainerView.hw_top = yPos;
|
||||
self.backgroundView.dimState = DimStateMax;
|
||||
} config:[self presentable] completion:^(BOOL completion) {
|
||||
self.isPresenting = NO;
|
||||
[[self presentable] panModalTransitionDidFinish];
|
||||
|
||||
if (@available(iOS 10.0, *)) {
|
||||
self.feedbackGenerator = nil;
|
||||
}
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
[self configureViewLayout];
|
||||
}
|
||||
|
||||
#pragma mark - public method
|
||||
|
||||
- (void)setNeedsLayoutUpdate {
|
||||
|
||||
[self configureViewLayout];
|
||||
[self updateBackgroundColor];
|
||||
[self.handler observeScrollable];
|
||||
[self adjustPresentedViewFrame];
|
||||
[self.handler configureScrollViewInsets];
|
||||
|
||||
[self updateContainerViewShadow];
|
||||
[self updateDragIndicatorView];
|
||||
[self updateRoundedCorners];
|
||||
}
|
||||
|
||||
- (void)updateUserHitBehavior {
|
||||
[self checkBackgroundViewEventPass];
|
||||
[self checkPanGestureRecognizer];
|
||||
}
|
||||
|
||||
- (void)transitionToState:(PresentationState)state animated:(BOOL)animated {
|
||||
|
||||
if (![self.presentable shouldTransitionToState:state]) return;
|
||||
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
[self.presentable willTransitionToState:state];
|
||||
|
||||
switch (state) {
|
||||
case PresentationStateLong: {
|
||||
[self snapToYPos:self.handler.longFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
case PresentationStateMedium: {
|
||||
[self snapToYPos:self.handler.mediumFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
case PresentationStateShort: {
|
||||
[self snapToYPos:self.handler.shortFormYPosition animated:animated];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
self.currentPresentationState = state;
|
||||
[[self presentable] didChangeTransitionToState:state];
|
||||
}
|
||||
|
||||
- (void)setScrollableContentOffset:(CGPoint)offset animated:(BOOL)animated {
|
||||
[self.handler setScrollableContentOffset:offset animated:animated];
|
||||
}
|
||||
|
||||
#pragma mark - layout
|
||||
|
||||
- (void)adjustPresentedViewFrame {
|
||||
CGRect frame = self.frame;
|
||||
CGSize size = CGSizeMake(CGRectGetWidth(frame), CGRectGetHeight(frame) - self.handler.anchoredYPosition);
|
||||
|
||||
self.panContainerView.hw_size = frame.size;
|
||||
self.panContainerView.contentView.frame = CGRectMake(0, 0, size.width, size.height);
|
||||
self.contentView.frame = self.panContainerView.contentView.bounds;
|
||||
[self.contentView setNeedsLayout];
|
||||
[self.contentView layoutIfNeeded];
|
||||
}
|
||||
|
||||
- (void)configureViewLayout {
|
||||
|
||||
[self.handler configureViewLayout];
|
||||
self.userInteractionEnabled = [[self presentable] isUserInteractionEnabled];
|
||||
}
|
||||
|
||||
- (void)layoutBackgroundView {
|
||||
[self addSubview:self.backgroundView];
|
||||
[self updateBackgroundColor];
|
||||
self.backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
NSArray *hCons = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
|
||||
NSArray *vCons = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView]|" options:0 metrics:nil views:@{@"backgroundView": self.backgroundView}];
|
||||
[NSLayoutConstraint activateConstraints:hCons];
|
||||
[NSLayoutConstraint activateConstraints:vCons];
|
||||
}
|
||||
|
||||
- (void)updateBackgroundColor {
|
||||
self.backgroundView.blurTintColor = [self.presentable backgroundConfig].blurTintColor;
|
||||
}
|
||||
|
||||
- (void)layoutPresentedView {
|
||||
if (!self.presentable)
|
||||
return;
|
||||
|
||||
self.handler.presentedView = self.panContainerView;
|
||||
|
||||
if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
[self.panContainerView addGestureRecognizer:self.handler.panGestureRecognizer];
|
||||
} else {
|
||||
[self addGestureRecognizer:self.handler.panGestureRecognizer];
|
||||
}
|
||||
|
||||
[self setNeedsLayoutUpdate];
|
||||
[self adjustPanContainerBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)adjustPanContainerBackgroundColor {
|
||||
self.panContainerView.contentView.backgroundColor = self.contentView.backgroundColor ? : [self.presentable panScrollable].backgroundColor;
|
||||
}
|
||||
|
||||
- (void)updateDragIndicatorView {
|
||||
if ([self.presentable showDragIndicator]) {
|
||||
[self addDragIndicatorView];
|
||||
} else {
|
||||
self.dragIndicatorView.hidden = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addDragIndicatorView {
|
||||
// if has been add, won't update it.
|
||||
self.dragIndicatorView.hidden = NO;
|
||||
|
||||
if (self.dragIndicatorView.superview == self.panContainerView) {
|
||||
[self updateDragIndicatorViewFrame];
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
return;
|
||||
}
|
||||
|
||||
self.handler.dragIndicatorView = self.dragIndicatorView;
|
||||
[self.panContainerView addSubview:self.dragIndicatorView];
|
||||
[self updateDragIndicatorViewFrame];
|
||||
|
||||
[self.dragIndicatorView setupSubviews];
|
||||
[self.dragIndicatorView didChangeToState:HWIndicatorStateNormal];
|
||||
}
|
||||
|
||||
- (void)updateDragIndicatorViewFrame {
|
||||
CGSize indicatorSize = [self.dragIndicatorView indicatorSize];
|
||||
self.dragIndicatorView.frame = CGRectMake((self.panContainerView.hw_width - indicatorSize.width) / 2, -kIndicatorYOffset - indicatorSize.height, indicatorSize.width, indicatorSize.height);
|
||||
}
|
||||
|
||||
- (void)updateContainerViewShadow {
|
||||
HWPanModalShadow *shadow = [[self presentable] contentShadow];
|
||||
if (shadow.shadowColor) {
|
||||
[self.panContainerView updateShadow:shadow.shadowColor shadowRadius:shadow.shadowRadius shadowOffset:shadow.shadowOffset shadowOpacity:shadow.shadowOpacity];
|
||||
} else {
|
||||
[self.panContainerView clearShadow];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateRoundedCorners {
|
||||
if ([self.presentable shouldRoundTopCorners]) {
|
||||
[self addRoundedCornersToView:self.panContainerView.contentView];
|
||||
} else {
|
||||
[self resetRoundedCornersToView:self.panContainerView.contentView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addRoundedCornersToView:(UIView *)view {
|
||||
CGFloat radius = [self.presentable cornerRadius];
|
||||
|
||||
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerTopLeft cornerRadii:CGSizeMake(radius, radius)];
|
||||
|
||||
CAShapeLayer *mask = [CAShapeLayer new];
|
||||
mask.path = bezierPath.CGPath;
|
||||
view.layer.mask = mask;
|
||||
|
||||
// 提高性能
|
||||
view.layer.shouldRasterize = YES;
|
||||
view.layer.rasterizationScale = [UIScreen mainScreen].scale;
|
||||
}
|
||||
|
||||
- (void)resetRoundedCornersToView:(UIView *)view {
|
||||
view.layer.mask = nil;
|
||||
view.layer.shouldRasterize = NO;
|
||||
}
|
||||
|
||||
- (void)snapToYPos:(CGFloat)yPos animated:(BOOL)animated {
|
||||
|
||||
if (animated) {
|
||||
[HWPanModalAnimator animate:^{
|
||||
self.isPresentedViewAnimating = YES;
|
||||
[self adjustToYPos:yPos];
|
||||
} config:self.presentable completion:^(BOOL completion) {
|
||||
self.isPresentedViewAnimating = NO;
|
||||
}];
|
||||
} else {
|
||||
[self adjustToYPos:yPos];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)adjustToYPos:(CGFloat)yPos {
|
||||
self.panContainerView.hw_top = MAX(yPos, self.handler.anchoredYPosition);
|
||||
|
||||
// change dim background starting from shortFormYPosition.
|
||||
if (self.panContainerView.frame.origin.y >= self.handler.shortFormYPosition) {
|
||||
|
||||
CGFloat yDistanceFromShortForm = self.panContainerView.frame.origin.y - self.handler.shortFormYPosition;
|
||||
CGFloat bottomHeight = self.hw_height - self.handler.shortFormYPosition;
|
||||
CGFloat percent = yDistanceFromShortForm / bottomHeight;
|
||||
self.backgroundView.dimState = DimStatePercent;
|
||||
self.backgroundView.percent = 1 - percent;
|
||||
|
||||
[self.presentable panModalGestureRecognizer:self.handler.panGestureRecognizer dismissPercent:MIN(percent, 1)];
|
||||
|
||||
} else {
|
||||
self.backgroundView.dimState = DimStateMax;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentableHandlerDelegate
|
||||
|
||||
- (void)adjustPresentableYPos:(CGFloat)yPos {
|
||||
[self adjustToYPos:yPos];
|
||||
}
|
||||
|
||||
- (void)presentableTransitionToState:(PresentationState)state {
|
||||
[self transitionToState:state animated:YES];
|
||||
}
|
||||
|
||||
- (PresentationState)getCurrentPresentationState {
|
||||
return self.currentPresentationState;
|
||||
}
|
||||
|
||||
- (void)dismiss:(BOOL)isInteractive mode:(PanModalInteractiveMode)mode {
|
||||
self.handler.panGestureRecognizer.enabled = NO;
|
||||
self.isDismissing = YES;
|
||||
|
||||
[[self presentable] panModalWillDismiss];
|
||||
|
||||
[HWPanModalAnimator animate:^{
|
||||
self.panContainerView.hw_top = CGRectGetHeight(self.bounds);
|
||||
self.backgroundView.dimState = DimStateOff;
|
||||
self.dragIndicatorView.alpha = 0;
|
||||
} config:[self presentable] completion:^(BOOL completion) {
|
||||
[self removeFromSuperview];
|
||||
[[self presentable] panModalDidDismissed];
|
||||
self.animationBlock ? self.animationBlock() : nil;
|
||||
self.isDismissing = NO;
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentableHandlerDataSource
|
||||
|
||||
- (CGSize)containerSize {
|
||||
return self.presentingView.bounds.size;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingDismissed {
|
||||
return self.isDismissing;
|
||||
}
|
||||
|
||||
- (BOOL)isBeingPresented {
|
||||
return self.isPresenting;
|
||||
}
|
||||
|
||||
- (BOOL)isFormPositionAnimating {
|
||||
return self.isPresentedViewAnimating;
|
||||
}
|
||||
|
||||
- (BOOL)isPresentedViewAnchored {
|
||||
|
||||
if (![[self presentable] shouldRespondToPanModalGestureRecognizer:self.handler.panGestureRecognizer]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (!self.isPresentedViewAnimating && self.handler.extendsPanScrolling && (CGRectGetMinY(self.panContainerView.frame) <= self.handler.anchoredYPosition || HW_TWO_FLOAT_IS_EQUAL(CGRectGetMinY(self.panContainerView.frame), self.handler.anchoredYPosition))) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - event handle
|
||||
|
||||
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
|
||||
if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self pointInside:point withEvent:event]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL eventThrough = [[self presentable] allowsTouchEventsPassingThroughTransitionView];
|
||||
if (eventThrough) {
|
||||
CGPoint convertedPoint = [self.panContainerView convertPoint:point fromView:self];
|
||||
if (CGRectGetWidth(self.panContainerView.frame) >= convertedPoint.x &&
|
||||
convertedPoint.x > 0 &&
|
||||
CGRectGetHeight(self.panContainerView.frame) >= convertedPoint.y &&
|
||||
convertedPoint.y > 0) {
|
||||
return [super hitTest:point withEvent:event];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
} else {
|
||||
return [super hitTest:point withEvent:event];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)checkBackgroundViewEventPass {
|
||||
if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
self.backgroundView.userInteractionEnabled = NO;
|
||||
self.backgroundView.tapBlock = nil;
|
||||
} else {
|
||||
self.backgroundView.userInteractionEnabled = YES;
|
||||
__weak typeof(self) wkSelf = self;
|
||||
self.backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
|
||||
if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
|
||||
[wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
- (void)checkPanGestureRecognizer {
|
||||
if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
[self removeGestureRecognizer:self.handler.panGestureRecognizer];
|
||||
[self.panContainerView addGestureRecognizer:self.handler.panGestureRecognizer];
|
||||
} else {
|
||||
[self.panContainerView removeGestureRecognizer:self.handler.panGestureRecognizer];
|
||||
[self addGestureRecognizer:self.handler.panGestureRecognizer];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - getter
|
||||
|
||||
- (id<HWPanModalPresentable>)presentable {
|
||||
if ([self.contentView conformsToProtocol:@protocol(HWPanModalPresentable)]) {
|
||||
return self.contentView;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (HWDimmedView *)backgroundView {
|
||||
if (!_backgroundView) {
|
||||
if (self.presentable) {
|
||||
_backgroundView = [[HWDimmedView alloc] initWithBackgroundConfig:[self.presentable backgroundConfig]];
|
||||
} else {
|
||||
_backgroundView = [[HWDimmedView alloc] init];
|
||||
}
|
||||
|
||||
if ([[self presentable] allowsTouchEventsPassingThroughTransitionView]) {
|
||||
_backgroundView.userInteractionEnabled = NO;
|
||||
} else {
|
||||
__weak typeof(self) wkSelf = self;
|
||||
_backgroundView.tapBlock = ^(UITapGestureRecognizer *recognizer) {
|
||||
if ([[wkSelf presentable] allowsTapBackgroundToDismiss]) {
|
||||
[wkSelf dismiss:NO mode:PanModalInteractiveModeNone];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return _backgroundView;
|
||||
}
|
||||
|
||||
- (HWPanContainerView *)panContainerView {
|
||||
if (!_panContainerView) {
|
||||
_panContainerView = [[HWPanContainerView alloc] initWithPresentedView:self.contentView frame:self.bounds];
|
||||
}
|
||||
|
||||
return _panContainerView;
|
||||
}
|
||||
|
||||
- (UIView<HWPanModalIndicatorProtocol> *)dragIndicatorView {
|
||||
|
||||
if (!_dragIndicatorView) {
|
||||
if ([self presentable] &&
|
||||
[[self presentable] respondsToSelector:@selector(customIndicatorView)] &&
|
||||
[[self presentable] customIndicatorView] != nil) {
|
||||
_dragIndicatorView = [[self presentable] customIndicatorView];
|
||||
// set the indicator size first in case `setupSubviews` can Not get the right size.
|
||||
_dragIndicatorView.hw_size = [[[self presentable] customIndicatorView] indicatorSize];
|
||||
} else {
|
||||
_dragIndicatorView = [HWPanIndicatorView new];
|
||||
}
|
||||
}
|
||||
|
||||
return _dragIndicatorView;
|
||||
}
|
||||
|
||||
@end
|
||||
36
Pods/HWPanModal/Sources/View/PanModal/HWPanModalContentView.h
generated
Normal file
36
Pods/HWPanModal/Sources/View/PanModal/HWPanModalContentView.h
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// HWPanModalContentView.h
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/17.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <HWPanModal/HWPanModalPresentable.h>
|
||||
#import <HWPanModal/HWPanModalPresentationUpdateProtocol.h>
|
||||
#import <HWPanModal/UIViewController+LayoutHelper.h>
|
||||
#import <HWPanModal/HWPanModalPanGestureDelegate.h>
|
||||
|
||||
@class HWPanModalContainerView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// when use `HWPanModalContentView`, you should take care of the safe area by yourself.
|
||||
@interface HWPanModalContentView : UIView <HWPanModalPresentable, HWPanModalPanGestureDelegate, HWPanModalPresentationUpdateProtocol, HWPanModalPresentableLayoutProtocol>
|
||||
|
||||
/**
|
||||
* present in the target view
|
||||
* @param view The view which present to. If the view is nil, will use UIWindow's keyWindow.
|
||||
*/
|
||||
- (void)presentInView:(nullable UIView *)view;
|
||||
|
||||
/**
|
||||
* call this method to dismiss contentView directly.
|
||||
* @param flag should animate flag
|
||||
* @param completion dismiss completion block
|
||||
*/
|
||||
- (void)dismissAnimated:(BOOL)flag completion:(void (^)(void))completion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
414
Pods/HWPanModal/Sources/View/PanModal/HWPanModalContentView.m
generated
Normal file
414
Pods/HWPanModal/Sources/View/PanModal/HWPanModalContentView.m
generated
Normal file
@@ -0,0 +1,414 @@
|
||||
//
|
||||
// HWPanModalContentView.m
|
||||
// Pods
|
||||
//
|
||||
// Created by heath wang on 2019/10/17.
|
||||
//
|
||||
|
||||
#import "HWPanModalContentView.h"
|
||||
#import "HWPanModalContainerView.h"
|
||||
|
||||
@interface HWPanModalContentView ()
|
||||
|
||||
@property (nonatomic, weak) HWPanModalContainerView *containerView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HWPanModalContentView
|
||||
|
||||
#pragma mark - public method
|
||||
|
||||
- (void)presentInView:(UIView *)view {
|
||||
if (!view) {
|
||||
view = [self findKeyWindow];
|
||||
}
|
||||
HWPanModalContainerView *containerView = [[HWPanModalContainerView alloc] initWithPresentingView:view contentView:self];
|
||||
[containerView show];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(BOOL)flag completion:(void (^)(void))completion {
|
||||
[self.containerView dismissAnimated:flag completion:completion];
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentationUpdateProtocol
|
||||
|
||||
- (void)hw_panModalTransitionTo:(PresentationState)state {
|
||||
[self.containerView transitionToState:state animated:YES];
|
||||
}
|
||||
|
||||
- (void)hw_panModalSetContentOffset:(CGPoint)offset {
|
||||
[self.containerView setScrollableContentOffset:offset animated:YES];
|
||||
}
|
||||
|
||||
- (void)hw_panModalSetNeedsLayoutUpdate {
|
||||
[self.containerView setNeedsLayoutUpdate];
|
||||
}
|
||||
|
||||
- (void)hw_panModalUpdateUserHitBehavior {
|
||||
[self.containerView updateUserHitBehavior];
|
||||
}
|
||||
|
||||
- (void)hw_panModalTransitionTo:(PresentationState)state animated:(BOOL)animated {
|
||||
[self.containerView transitionToState:state animated:animated];
|
||||
}
|
||||
|
||||
- (void)hw_panModalSetContentOffset:(CGPoint)offset animated:(BOOL)animated {
|
||||
[self.containerView setScrollableContentOffset:offset animated:animated];
|
||||
}
|
||||
|
||||
- (void)hw_dismissAnimated:(BOOL)animated completion:(void (^)(void))completion {
|
||||
[self dismissAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (HWDimmedView *)hw_dimmedView {
|
||||
return self.containerView.backgroundView;
|
||||
}
|
||||
|
||||
- (UIView *)hw_rootContainerView {
|
||||
return self.containerView;
|
||||
}
|
||||
|
||||
- (UIView *)hw_contentView {
|
||||
return (UIView *)self.containerView.panContainerView;
|
||||
}
|
||||
|
||||
- (PresentationState)hw_presentationState {
|
||||
return self.containerView.currentPresentationState;
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentable
|
||||
|
||||
- (UIScrollView *)panScrollable {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGFloat)topOffset {
|
||||
return self.topLayoutOffset + 21.f;
|
||||
}
|
||||
|
||||
- (PanModalHeight)shortFormHeight {
|
||||
return [self longFormHeight];
|
||||
}
|
||||
|
||||
- (PanModalHeight)mediumFormHeight {
|
||||
return [self longFormHeight];
|
||||
}
|
||||
|
||||
- (PanModalHeight)longFormHeight {
|
||||
if ([self panScrollable]) {
|
||||
[[self panScrollable] layoutIfNeeded];
|
||||
return PanModalHeightMake(PanModalHeightTypeContent, MAX([self panScrollable].contentSize.height, [self panScrollable].bounds.size.height));
|
||||
} else {
|
||||
return PanModalHeightMake(PanModalHeightTypeMax, 0);
|
||||
}
|
||||
}
|
||||
|
||||
- (PresentationState)originPresentationState {
|
||||
return PresentationStateShort;
|
||||
}
|
||||
|
||||
- (CGFloat)springDamping {
|
||||
return 0.8;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)transitionDuration {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)dismissalDuration {
|
||||
return [self transitionDuration];
|
||||
}
|
||||
|
||||
- (UIViewAnimationOptions)transitionAnimationOptions {
|
||||
return UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState;
|
||||
}
|
||||
|
||||
- (CGFloat)backgroundAlpha {
|
||||
return 0.7;
|
||||
}
|
||||
|
||||
- (CGFloat)backgroundBlurRadius {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (nonnull UIColor *)backgroundBlurColor {
|
||||
return [UIColor whiteColor];
|
||||
}
|
||||
|
||||
- (HWBackgroundConfig *)backgroundConfig {
|
||||
return [HWBackgroundConfig configWithBehavior:HWBackgroundBehaviorDefault];
|
||||
}
|
||||
|
||||
- (UIEdgeInsets)scrollIndicatorInsets {
|
||||
CGFloat top = [self shouldRoundTopCorners] ? [self cornerRadius] : 0;
|
||||
return UIEdgeInsetsMake(top, 0, self.bottomLayoutOffset, 0);
|
||||
}
|
||||
|
||||
- (BOOL)showsScrollableVerticalScrollIndicator {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutoSetPanScrollContentInset {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)anchorModalToLongForm {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)allowsExtendedPanScrolling {
|
||||
if ([self panScrollable]) {
|
||||
UIScrollView *scrollable = [self panScrollable];
|
||||
[scrollable layoutIfNeeded];
|
||||
return scrollable.contentSize.height > (scrollable.frame.size.height - self.bottomLayoutOffset);
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)allowsDragToDismiss {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)minVerticalVelocityToTriggerDismiss {
|
||||
return 300;
|
||||
}
|
||||
|
||||
- (BOOL)allowsTapBackgroundToDismiss {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)allowsPullDownWhenShortState {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)allowScreenEdgeInteractive {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (CGFloat)maxAllowedDistanceToLeftScreenEdgeForPanInteraction {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (CGFloat)minHorizontalVelocityToTriggerScreenEdgeDismiss {
|
||||
return 500;
|
||||
}
|
||||
|
||||
- (PresentingViewControllerAnimationStyle)presentingVCAnimationStyle {
|
||||
return PresentingViewControllerAnimationStyleNone;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAnimatePresentingVC {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (id <HWPresentingViewControllerAnimatedTransitioning>)customPresentingVCAnimation {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isPanScrollEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isUserInteractionEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isHapticFeedbackEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)allowsTouchEventsPassingThroughTransitionView {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldRoundTopCorners {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)cornerRadius {
|
||||
return 8;
|
||||
}
|
||||
|
||||
- (HWPanModalShadow *)contentShadow {
|
||||
return [HWPanModalShadow panModalShadowNil];
|
||||
}
|
||||
|
||||
- (BOOL)showDragIndicator {
|
||||
if ([self allowsTouchEventsPassingThroughTransitionView]) {
|
||||
return NO;
|
||||
}
|
||||
return [self shouldRoundTopCorners];
|
||||
}
|
||||
|
||||
- (nullable UIView <HWPanModalIndicatorProtocol> *)customIndicatorView {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isAutoHandleKeyboardEnabled {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)keyboardOffsetFromInputView {
|
||||
return 5;
|
||||
}
|
||||
|
||||
- (BOOL)shouldRespondToPanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)willRespondToPanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
}
|
||||
|
||||
- (void)didRespondToPanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
}
|
||||
|
||||
- (void)didEndRespondToPanModalGestureRecognizer:(nonnull UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
|
||||
}
|
||||
|
||||
- (BOOL)shouldPrioritizePanModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldTransitionToState:(PresentationState)state {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)willTransitionToState:(PresentationState)state {
|
||||
|
||||
}
|
||||
|
||||
- (void)didChangeTransitionToState:(PresentationState)state {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalGestureRecognizer:(UIPanGestureRecognizer *)panGestureRecognizer dismissPercent:(CGFloat)percent {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalWillDismiss {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalDidDismissed {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalTransitionWillBegin {
|
||||
|
||||
}
|
||||
|
||||
- (void)panModalTransitionDidFinish {
|
||||
|
||||
}
|
||||
|
||||
- (void)presentedViewDidMoveToSuperView {
|
||||
|
||||
}
|
||||
|
||||
- (BOOL)shouldEnableAppearanceTransition {
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - HWPanModalPresentableLayoutProtocol
|
||||
|
||||
- (CGFloat)topLayoutOffset {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (CGFloat)bottomLayoutOffset {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (CGFloat)shortFormYPos {
|
||||
CGFloat shortFormYPos = [self topMarginFromPanModalHeight:[self shortFormHeight]] + [self topOffset];
|
||||
return MAX(shortFormYPos, self.longFormYPos);
|
||||
}
|
||||
|
||||
- (CGFloat)mediumFormYPos {
|
||||
CGFloat mediumFormYPos = [self topMarginFromPanModalHeight:[self mediumFormHeight]] + [self topOffset];
|
||||
return MAX(mediumFormYPos, self.longFormYPos);
|
||||
}
|
||||
|
||||
- (CGFloat)longFormYPos {
|
||||
CGFloat longFrom = MAX([self topMarginFromPanModalHeight:[self longFormHeight]], [self topMarginFromPanModalHeight:PanModalHeightMake(PanModalHeightTypeMax, 0)]) + [self topOffset];
|
||||
return longFrom;
|
||||
}
|
||||
|
||||
- (CGFloat)bottomYPos {
|
||||
if (self.containerView) {
|
||||
return self.containerView.bounds.size.height - [self topOffset];
|
||||
}
|
||||
return self.bounds.size.height;
|
||||
}
|
||||
|
||||
- (CGFloat)topMarginFromPanModalHeight:(PanModalHeight)panModalHeight {
|
||||
switch (panModalHeight.heightType) {
|
||||
case PanModalHeightTypeMax:
|
||||
return 0.0f;
|
||||
case PanModalHeightTypeMaxTopInset:
|
||||
return panModalHeight.height;
|
||||
case PanModalHeightTypeContent:
|
||||
return self.bottomYPos - (panModalHeight.height + self.bottomLayoutOffset);
|
||||
case PanModalHeightTypeContentIgnoringSafeArea:
|
||||
return self.bottomYPos - panModalHeight.height;
|
||||
case PanModalHeightTypeIntrinsic: {
|
||||
[self layoutIfNeeded];
|
||||
|
||||
CGSize targetSize = CGSizeMake(self.containerView ? self.containerView.bounds.size.width : [UIScreen mainScreen].bounds.size.width, UILayoutFittingCompressedSize.height);
|
||||
CGFloat intrinsicHeight = [self systemLayoutSizeFittingSize:targetSize].height;
|
||||
return self.bottomYPos - (intrinsicHeight + self.bottomLayoutOffset);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
- (HWPanModalContainerView *)containerView {
|
||||
// we assume the container view will not change after we got it.
|
||||
if (!_containerView) {
|
||||
UIView *fatherView = self.superview;
|
||||
while (fatherView) {
|
||||
if ([fatherView isKindOfClass:HWPanModalContainerView.class]) {
|
||||
_containerView = (HWPanModalContainerView *) fatherView;
|
||||
break;
|
||||
}
|
||||
fatherView = fatherView.superview;
|
||||
}
|
||||
}
|
||||
|
||||
return _containerView;
|
||||
}
|
||||
|
||||
- (UIView *)findKeyWindow {
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
NSSet<UIScene *> *connectedScenes = [UIApplication sharedApplication].connectedScenes;
|
||||
for (UIScene *scene in connectedScenes) {
|
||||
if ([scene isKindOfClass:UIWindowScene.class]) {
|
||||
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
||||
for (UIWindow *tmpWindow in windowScene.windows) {
|
||||
if ([tmpWindow isKeyWindow]) {
|
||||
return tmpWindow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
NSArray *windows = [UIApplication sharedApplication].windows;
|
||||
for (UIWindow *window in windows) {
|
||||
if ([window isKeyWindow]) {
|
||||
return window;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
21
Pods/JXCategoryView/LICENSE
generated
Normal file
21
Pods/JXCategoryView/LICENSE
generated
Normal 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
296
Pods/JXCategoryView/README.md
generated
Normal file
@@ -0,0 +1,296 @@
|
||||
<div align=center><img src="Example/Example/Images/JXCategoryView.png" width="405" height="63" /></div>
|
||||
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://cocoapods.org/pods/JXCategoryView)
|
||||
[](#)
|
||||
|
||||
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.
|
||||
29
Pods/JXCategoryView/Sources/Base/JXCategoryBaseCell.h
generated
Normal file
29
Pods/JXCategoryView/Sources/Base/JXCategoryBaseCell.h
generated
Normal 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
|
||||
98
Pods/JXCategoryView/Sources/Base/JXCategoryBaseCell.m
generated
Normal file
98
Pods/JXCategoryView/Sources/Base/JXCategoryBaseCell.m
generated
Normal 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
|
||||
31
Pods/JXCategoryView/Sources/Base/JXCategoryBaseCellModel.h
generated
Normal file
31
Pods/JXCategoryView/Sources/Base/JXCategoryBaseCellModel.h
generated
Normal 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
|
||||
13
Pods/JXCategoryView/Sources/Base/JXCategoryBaseCellModel.m
generated
Normal file
13
Pods/JXCategoryView/Sources/Base/JXCategoryBaseCellModel.m
generated
Normal 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
|
||||
222
Pods/JXCategoryView/Sources/Base/JXCategoryBaseView.h
generated
Normal file
222
Pods/JXCategoryView/Sources/Base/JXCategoryBaseView.h
generated
Normal 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.2,cellWidthZoomEnabled为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
|
||||
697
Pods/JXCategoryView/Sources/Base/JXCategoryBaseView.m
generated
Normal file
697
Pods/JXCategoryView/Sources/Base/JXCategoryBaseView.m
generated
Normal 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;
|
||||
// 正在滚动中的目标index。用于处理正在滚动列表的时候,立即点击item,会导致界面显示异常。
|
||||
@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];
|
||||
|
||||
//部分使用者为了适配不同的手机屏幕尺寸,JXCategoryView的宽高比要求保持一样,所以它的高度就会因为不同宽度的屏幕而不一样。计算出来的高度,有时候会是位数很长的浮点数,如果把这个高度设置给UICollectionView就会触发内部的一个错误。所以,为了规避这个问题,在这里对高度统一向下取整。
|
||||
//如果向下取整导致了你的页面异常,请自己重新设置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;
|
||||
//如果内容左边距是Automatic,就加1
|
||||
if (self.contentEdgeInsetLeft == JXCategoryViewAutomaticDimension) {
|
||||
cellSpacingItemCount += 1;
|
||||
}else {
|
||||
totalCellSpacingWidth -= self.contentEdgeInsetLeft;
|
||||
}
|
||||
//如果内容右边距是Automatic,就加1
|
||||
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到当前选中的位置----------------------
|
||||
//因为初始化的时候,collectionView并没有初始化完,cell都没有被加载出来。只有自己手动计算当前选中的index的位置,然后更新到contentOffset
|
||||
__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) {
|
||||
//某些情况系统会出现JXCategoryView先布局,contentScrollView后布局。就会导致下面指定defaultSelectedIndex失效,所以发现contentScrollView的frame为zero时,强行触发其父视图链里面已经有frame的一个父视图的layoutSubviews方法。
|
||||
//比如JXSegmentedListContainerView会将contentScrollView包裹起来使用,该情况需要JXSegmentedListContainerView.superView触发布局更新
|
||||
UIView *parentView = self.contentScrollView.superview;
|
||||
while (parentView != nil && CGRectEqualToRect(parentView.frame, CGRectZero)) {
|
||||
parentView = parentView.superview;
|
||||
}
|
||||
[parentView setNeedsLayout];
|
||||
[parentView layoutIfNeeded];
|
||||
}
|
||||
//将contentScrollView的contentOffset定位到当前选中index的位置
|
||||
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) {
|
||||
//目标index和当前选中的index相等,就不需要处理后续的选中更新逻辑,只需要回调代理方法即可。
|
||||
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];
|
||||
//延时为了解决cellwidth变化,点击最后几个cell,scrollToItem会出现位置偏移bu。需要等cellWidth动画渐变结束后再滚动到index的cell位置。
|
||||
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.x为0
|
||||
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.x为maxContentOffsetX
|
||||
return;
|
||||
}
|
||||
ratio = MAX(0, MIN(self.dataSource.count - 1, ratio));
|
||||
NSInteger baseIndex = floorf(ratio);
|
||||
CGFloat remainderRatio = ratio - baseIndex;
|
||||
|
||||
if (remainderRatio == 0) {
|
||||
//快速滑动翻页,用户一直在拖拽contentScrollView,需要更新选中状态
|
||||
//滑动一小段距离,然后放开回到原位,contentOffset同样的值会回调多次。例如在index为1的情况,滑动放开回到原位,contentOffset会多次回调CGPoint(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
//快速滑动翻页,当remainderRatio没有变成0,但是已经翻页了,需要通过下面的判断,触发选中
|
||||
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
|
||||
24
Pods/JXCategoryView/Sources/Common/JXCategoryCollectionView.h
generated
Normal file
24
Pods/JXCategoryView/Sources/Common/JXCategoryCollectionView.h
generated
Normal 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
|
||||
52
Pods/JXCategoryView/Sources/Common/JXCategoryCollectionView.m
generated
Normal file
52
Pods/JXCategoryView/Sources/Common/JXCategoryCollectionView.m
generated
Normal 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
|
||||
18
Pods/JXCategoryView/Sources/Common/JXCategoryFactory.h
generated
Normal file
18
Pods/JXCategoryView/Sources/Common/JXCategoryFactory.h
generated
Normal 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
|
||||
29
Pods/JXCategoryView/Sources/Common/JXCategoryFactory.m
generated
Normal file
29
Pods/JXCategoryView/Sources/Common/JXCategoryFactory.m
generated
Normal 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
|
||||
31
Pods/JXCategoryView/Sources/Common/JXCategoryIndicatorParamsModel.h
generated
Normal file
31
Pods/JXCategoryView/Sources/Common/JXCategoryIndicatorParamsModel.h
generated
Normal 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
|
||||
13
Pods/JXCategoryView/Sources/Common/JXCategoryIndicatorParamsModel.m
generated
Normal file
13
Pods/JXCategoryView/Sources/Common/JXCategoryIndicatorParamsModel.m
generated
Normal 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
|
||||
49
Pods/JXCategoryView/Sources/Common/JXCategoryIndicatorProtocol.h
generated
Normal file
49
Pods/JXCategoryView/Sources/Common/JXCategoryIndicatorProtocol.h
generated
Normal 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
|
||||
16
Pods/JXCategoryView/Sources/Common/JXCategoryListContainerRTLCell.h
generated
Normal file
16
Pods/JXCategoryView/Sources/Common/JXCategoryListContainerRTLCell.h
generated
Normal 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
|
||||
23
Pods/JXCategoryView/Sources/Common/JXCategoryListContainerRTLCell.m
generated
Normal file
23
Pods/JXCategoryView/Sources/Common/JXCategoryListContainerRTLCell.m
generated
Normal 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
|
||||
122
Pods/JXCategoryView/Sources/Common/JXCategoryListContainerView.h
generated
Normal file
122
Pods/JXCategoryView/Sources/Common/JXCategoryListContainerView.h
generated
Normal 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是index,value是对应的列表
|
||||
@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
|
||||
|
||||
547
Pods/JXCategoryView/Sources/Common/JXCategoryListContainerView.m
generated
Normal file
547
Pods/JXCategoryView/Sources/Common/JXCategoryListContainerView.m
generated
Normal 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:如果list是UIViewController,如果这里的frame修改是`[list listView].frame = cell.bounds;`。那么就必须给list vc添加如下代码:
|
||||
//- (void)loadView {
|
||||
// self.view = [[UIView alloc] init];
|
||||
//}
|
||||
//所以,总感觉是把UIViewController当做普通view使用,导致了系统内部的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
|
||||
24
Pods/JXCategoryView/Sources/Common/JXCategoryViewAnimator.h
generated
Normal file
24
Pods/JXCategoryView/Sources/Common/JXCategoryViewAnimator.h
generated
Normal 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
|
||||
|
||||
75
Pods/JXCategoryView/Sources/Common/JXCategoryViewAnimator.m
generated
Normal file
75
Pods/JXCategoryView/Sources/Common/JXCategoryViewAnimator.m
generated
Normal 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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user