diff --git a/QMUI/.github/ISSUE_TEMPLATE/bug_report.md b/QMUI/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 1f3a44f1..00000000
--- a/QMUI/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-name: Bug
-about: QMUIKit 框架本身的 bug(请注意区分非 QMUIKit 代码引发的问题)
-title: ''
-labels: 'bug'
-assignees: ''
-
----
-
-**Bug 表现**
-问题的具体描述
-
-**截图**
-Bug 现场的界面截图,或者 Xcode 控制台的错误信息截图,有问题的代码截图
-
-**如何重现**
-1. ...
-2. ...
-
-**预期的表现**
-正常情况下,应该是什么表现
-
-**其他信息**
- - 设备: [例如模拟器、iPhone、iPad]
- - iOS 版本: [iOS 12.x]
- - Xcode 版本: [Xcode 10.x]
- - QMUI 版本: [3.x.x]
diff --git a/QMUI/.github/ISSUE_TEMPLATE/feature_request.md b/QMUI/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 98765b71..00000000
--- a/QMUI/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-name: 意见与建议
-about: 功能、接口设计等的相关建议
-title: ''
-labels: 'suggest'
-assignees: ''
-
----
-
-**现存问题或期望目标**
-对于功能的建议,请说明具体的场景,现在的代码为什么无法实现需求。
-对于代码设计方面的建议,请说明目前的问题所在。
diff --git a/QMUI/.gitignore b/QMUI/.gitignore
index 82ed296f..1d74e0bf 100644
--- a/QMUI/.gitignore
+++ b/QMUI/.gitignore
@@ -1,4 +1,4 @@
.DS_Store
-xcuserdata/
-
+.xcuserdatad
+xcuserdata
diff --git a/QMUI/LICENSE.TXT b/QMUI/LICENSE.TXT
index b6931290..485d8bd0 100644
--- a/QMUI/LICENSE.TXT
+++ b/QMUI/LICENSE.TXT
@@ -1,5 +1,5 @@
Tencent is pleased to support the open source community by making QMUI_iOS available.
-Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
If you have downloaded a copy of the QMUI_iOS binary from Tencent, please note that the QMUI_iOS binary is licensed under the MIT License.
If you have downloaded a copy of the QMUI_iOS source code from Tencent, please note that QMUI_iOS source code is licensed under the MIT License. Your integration of QMUI_iOS into your own projects may require compliance with the MIT License.
A copy of the MIT License is included in this file.
diff --git a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h
index b102380c..543a96f4 100644
--- a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h
+++ b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIConfigurationTemplate.h
diff --git a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m
index 52ada5e7..74e36837 100644
--- a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m
+++ b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIConfigurationTemplate.m
@@ -42,7 +42,7 @@ - (void)applyConfigurationTemplate {
QMUICMI.backgroundColor = nil; // UIColorForBackground : 界面背景色,默认用于 QMUICommonViewController.view 的背景色
QMUICMI.maskDarkColor = UIColorMakeWithRGBA(0, 0, 0, .35f); // UIColorMask : 深色的背景遮罩,默认用于 QMAlertController、QMUIDialogViewController 等弹出控件的遮罩
QMUICMI.maskLightColor = UIColorMakeWithRGBA(255, 255, 255, .5f); // UIColorMaskWhite : 浅色的背景遮罩,QMUIKit 里默认没用到,只是占个位
- QMUICMI.separatorColor = UIColorMake(222, 224, 226); // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUI_Border) 分隔线颜色
+ QMUICMI.separatorColor = UIColorMake(222, 224, 226); // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUIBorder) 分隔线颜色
QMUICMI.separatorDashedColor = UIColorMake(17, 17, 17); // UIColorSeparatorDashed : 全局默认的虚线分隔线的颜色,默认 QMUIKit 暂时没用到
QMUICMI.placeholderColor = UIColorMake(196, 200, 208); // UIColorPlaceholder,全局的输入框的 placeholder 颜色,默认用于 QMUITextField、QMUITextView,不影响系统 UIKit 的输入框
@@ -51,6 +51,11 @@ - (void)applyConfigurationTemplate {
QMUICMI.testColorGreen = UIColorMakeWithRGBA(0, 255, 0, .3);
QMUICMI.testColorBlue = UIColorMakeWithRGBA(0, 0, 255, .3);
+ #pragma mark - QMUILog
+ QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log
+ QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log
+ QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintWarnLog : 是否允许输出 QMUILogLevelWarn 级别的 log
+ QMUICMI.shouldPrintQMUIWarnLogToConsole = NO; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上
#pragma mark - UIControl
@@ -62,36 +67,40 @@ - (void)applyConfigurationTemplate {
QMUICMI.buttonDisabledAlpha = UIControlDisabledAlpha; // ButtonDisabledAlpha : QMUIButton 在 disabled 时的 alpha,不影响系统的 UIButton
QMUICMI.buttonTintColor = UIColorBlue; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton
- QMUICMI.ghostButtonColorBlue = UIColorBlue; // GhostButtonColorBlue : QMUIGhostButtonColorBlue 的颜色
- QMUICMI.ghostButtonColorRed = UIColorRed; // GhostButtonColorRed : QMUIGhostButtonColorRed 的颜色
- QMUICMI.ghostButtonColorGreen = UIColorGreen; // GhostButtonColorGreen : QMUIGhostButtonColorGreen 的颜色
- QMUICMI.ghostButtonColorGray = UIColorGray; // GhostButtonColorGray : QMUIGhostButtonColorGray 的颜色
- QMUICMI.ghostButtonColorWhite = UIColorWhite; // GhostButtonColorWhite : QMUIGhostButtonColorWhite 的颜色
-
- QMUICMI.fillButtonColorBlue = UIColorBlue; // FillButtonColorBlue : QMUIFillButtonColorBlue 的颜色
- QMUICMI.fillButtonColorRed = UIColorRed; // FillButtonColorRed : QMUIFillButtonColorRed 的颜色
- QMUICMI.fillButtonColorGreen = UIColorGreen; // FillButtonColorGreen : QMUIFillButtonColorGreen 的颜色
- QMUICMI.fillButtonColorGray = UIColorGray; // FillButtonColorGray : QMUIFillButtonColorGray 的颜色
- QMUICMI.fillButtonColorWhite = UIColorWhite; // FillButtonColorWhite : QMUIFillButtonColorWhite 的颜色
-
- #pragma mark - TextField & TextView
+ #pragma mark - TextInput
+ QMUICMI.textFieldTextColor = nil; // TextFieldTextColor : QMUITextField、QMUITextView 的 textColor,不影响 UIKit 的输入框
QMUICMI.textFieldTintColor = nil; // TextFieldTintColor : QMUITextField、QMUITextView 的 tintColor,不影响 UIKit 的输入框
QMUICMI.textFieldTextInsets = UIEdgeInsetsMake(0, 7, 0, 7); // TextFieldTextInsets : QMUITextField 的内边距,不影响 UITextField
+ QMUICMI.keyboardAppearance = UIKeyboardAppearanceDefault; // KeyboardAppearance : UITextView、UITextField、UISearchBar 的 keyboardAppearance
+
+ #pragma mark - UISwitch
+ QMUICMI.switchOnTintColor = nil; // SwitchOnTintColor : UISwitch 打开时的背景色(除了圆点外的其他颜色)
+ QMUICMI.switchOffTintColor = nil; // SwitchOffTintColor : UISwitch 关闭时的背景色(除了圆点外的其他颜色)
+ QMUICMI.switchThumbTintColor = nil; // SwitchThumbTintColor : UISwitch 中间的操控圆点的颜色
#pragma mark - NavigationBar
+ if (@available(iOS 15.0, *)) {
+ QMUICMI.navBarUsesStandardAppearanceOnly = NO; // NavBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UINavigationBar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。
+ }
+ QMUICMI.navBarContainerClasses = nil; // NavBarContainerClasses : NavigationBar 系列开关被用于 UIAppearance 时的生效范围(默认情况下除了用于 UIAppearance 外,还用于实现了 QMUINavigationControllerAppearanceDelegate 的 UIViewController),默认为 nil。当赋值为 nil 或者空数组时等效于 @[UINavigationController.class],也即对所有 UINavigationBar 生效,包括系统的通讯录(ContactsUI.framework)、打印等。当值不为空时,获取 UINavigationBar 的 appearance 请使用 UINavigationBar.qmui_appearanceConfigured 方法代替系统的 UINavigationBar.appearance。请保证这个配置项先于其他任意 NavBar 配置项执行。
QMUICMI.navBarHighlightedAlpha = 0.2f; // NavBarHighlightedAlpha : QMUINavigationButton 在 highlighted 时的 alpha
QMUICMI.navBarDisabledAlpha = 0.2f; // NavBarDisabledAlpha : QMUINavigationButton 在 disabled 时的 alpha
- QMUICMI.navBarButtonFont = nil; // NavBarButtonFont : QMUINavigationButtonTypeNormal 和 UINavigationBar 上的 UIBarButtonItem 的字体
- QMUICMI.navBarButtonFontBold = nil; // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体
+ QMUICMI.navBarButtonFont = nil; // NavBarButtonFont : UINavigationBar 里 UIBarButtonItem 以及 QMUINavigationButtonTypeNormal 的字体
+ QMUICMI.navBarButtonFontBold = nil; // NavBarButtonFontBold : iOS 15 及以后用于设置 UINavigationBar 里 Done 类型的 UIBarButtonItem 以及 QMUINavigationButtonTypeBold 的字体,iOS 14 及以前只对后者生效
QMUICMI.navBarBackgroundImage = nil; // NavBarBackgroundImage : UINavigationBar 的背景图
- QMUICMI.navBarShadowImage = nil; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线
+ if (@available(iOS 15.0, *)) {
+ QMUICMI.navBarRemoveBackgroundEffectAutomatically = NO; // NavBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UINavigationBar 使用的是 UINavigationBarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。
+ }
+ QMUICMI.navBarShadowImage = nil; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线,配合 NavBarShadowImageColor 使用。
+ QMUICMI.navBarShadowImageColor = nil; // NavBarShadowImageColor : UINavigationBar.shadowImage 的颜色,如果为 nil,则使用 NavBarShadowImage 的值,如果 NavBarShadowImage 也为 nil,则使用系统默认的分隔线。如果不为 nil,而 NavBarShadowImage 为 nil,则自动创建一张 1px 高的图并将其设置为 NavBarShadowImageColor 的颜色然后设置上去,如果 NavBarShadowImage 不为 nil 且 renderingMode 不为 UIImageRenderingModeAlwaysOriginal,则将 NavBarShadowImage 设置为 NavBarShadowImageColor 的颜色然后设置上去。
QMUICMI.navBarBarTintColor = nil; // NavBarBarTintColor : UINavigationBar.barTintColor,也即背景色
- QMUICMI.navBarTintColor = nil; // NavBarTintColor : QMUINavigationController.navigationBar 的 tintColor,也即导航栏上面的按钮颜色,由于 tintColor 不支持 appearance,所以这里只支持 QMUINavigationController
+ QMUICMI.navBarStyle = UIBarStyleDefault; // NavBarStyle : UINavigationBar 的 barStyle
+ QMUICMI.navBarTintColor = nil; // NavBarTintColor : NavBarContainerClasses 里的 UINavigationBar 的 tintColor,也即导航栏上面的按钮颜色
QMUICMI.navBarTitleColor = nil; // NavBarTitleColor : UINavigationBar 的标题颜色,以及 QMUINavigationTitleView 的默认文字颜色
QMUICMI.navBarTitleFont = nil; // NavBarTitleFont : UINavigationBar 的标题字体,以及 QMUINavigationTitleView 的默认字体
- QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色,仅在 iOS 11 之后才有效
- QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体,仅在 iOS 11 之后才有效
+ QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色
+ QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体
QMUICMI.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移
QMUICMI.sizeNavBarBackIndicatorImageAutomatically = YES; // SizeNavBarBackIndicatorImageAutomatically : 是否要自动调整 NavBarBackIndicatorImage 的 size 为 (13, 21)
QMUICMI.navBarBackIndicatorImage = nil; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片,图片尺寸建议为(13, 21),否则最终的图片位置无法与系统原生的位置保持一致
@@ -104,46 +113,62 @@ - (void)applyConfigurationTemplate {
#pragma mark - TabBar
+ if (@available(iOS 15.0, *)) {
+ QMUICMI.tabBarUsesStandardAppearanceOnly = NO; // TabBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UITabBar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。
+ }
+ QMUICMI.tabBarContainerClasses = nil; // TabBarContainerClasses : TabBar 系列开关的生效范围,默认为 nil,当赋值为 nil 或者空数组时等效于 @[UITabBarController.class],也即对所有 UITabBar 生效。当值不为空时,获取 UITabBar 的 appearance 请使用 UITabBar.qmui_appearanceConfigured 方法代替系统的 UITabBar.appearance。请保证这个配置项先于其他任意 TabBar 配置项执行。
QMUICMI.tabBarBackgroundImage = nil; // TabBarBackgroundImage : UITabBar 的背景图
- QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor
+ if (@available(iOS 15.0, *)) {
+ QMUICMI.tabBarRemoveBackgroundEffectAutomatically = NO; // TabBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UITabBar 使用的是 UITabBarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。
+ }
+ QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor,如果需要看到磨砂效果则应该提供半透明的色值
QMUICMI.tabBarShadowImageColor = nil; // TabBarShadowImageColor : UITabBar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片
- QMUICMI.tabBarTintColor = nil; // TabBarTintColor : UITabBar 的 tintColor
- QMUICMI.tabBarItemTitleColor = nil; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色
- QMUICMI.tabBarItemTitleColorSelected = TabBarTintColor; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色
+ QMUICMI.tabBarStyle = UIBarStyleDefault; // TabBarStyle : UITabBar 的 barStyle
QMUICMI.tabBarItemTitleFont = nil; // TabBarItemTitleFont : UITabBarItem 的标题字体
+ QMUICMI.tabBarItemTitleFontSelected = nil; // TabBarItemTitleFontSelected : 选中的 UITabBarItem 的标题字体
+ QMUICMI.tabBarItemTitleColor = nil; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色
+ QMUICMI.tabBarItemTitleColorSelected = nil; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色
+ QMUICMI.tabBarItemImageColor = nil; // TabBarItemImageColor : UITabBarItem 未选中时的图片颜色
+ QMUICMI.tabBarItemImageColorSelected = nil; // TabBarItemImageColorSelected : UITabBarItem 选中时的图片颜色
#pragma mark - Toolbar
+ if (@available(iOS 15.0, *)) {
+ QMUICMI.toolBarUsesStandardAppearanceOnly = NO; // ToolBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UIToolbar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。
+ }
+ QMUICMI.toolBarContainerClasses = nil; // ToolBarContainerClasses : ToolBar 系列开关的生效范围,默认为 nil,当赋值为 nil 或者空数组时等效于 @[UINavigationController.class],也即对所有 UIToolbar 生效。当值不为空时,获取 UIToolbar 的 appearance 请使用 UIToolbar.qmui_appearanceConfigured 方法代替系统的 UIToolbar.appearance。请保证这个配置项先于其他任意 ToolBar 配置项执行。
QMUICMI.toolBarHighlightedAlpha = 0.4f; // ToolBarHighlightedAlpha : QMUIToolbarButton 在 highlighted 状态下的 alpha
QMUICMI.toolBarDisabledAlpha = 0.4f; // ToolBarDisabledAlpha : QMUIToolbarButton 在 disabled 状态下的 alpha
- QMUICMI.toolBarTintColor = nil; // ToolBarTintColor : UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色
+ QMUICMI.toolBarTintColor = nil; // ToolBarTintColor : NavBarContainerClasses 里的 UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色
QMUICMI.toolBarTintColorHighlighted = [ToolBarTintColor colorWithAlphaComponent:ToolBarHighlightedAlpha]; // ToolBarTintColorHighlighted : QMUIToolbarButton 在 highlighted 状态下的文字颜色
QMUICMI.toolBarTintColorDisabled = [ToolBarTintColor colorWithAlphaComponent:ToolBarDisabledAlpha]; // ToolBarTintColorDisabled : QMUIToolbarButton 在 disabled 状态下的文字颜色
- QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : UIToolbar 的背景图
- QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : UIToolbar 的 tintColor
- QMUICMI.toolBarShadowImageColor = nil; // ToolBarShadowImageColor : UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片
+ QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : NavBarContainerClasses 里的 UIToolbar 的背景图
+ if (@available(iOS 15.0, *)) {
+ QMUICMI.toolBarRemoveBackgroundEffectAutomatically = NO; // ToolBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UIToolbar 使用的是 UIToolbarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。
+ }
+ QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : NavBarContainerClasses 里的 UIToolbar 的 tintColor
+ QMUICMI.toolBarShadowImageColor = nil; // ToolBarShadowImageColor : NavBarContainerClasses 里的 UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片
+ QMUICMI.toolBarStyle = UIBarStyleDefault; // ToolBarStyle : NavBarContainerClasses 里的 UIToolbar 的 barStyle
QMUICMI.toolBarButtonFont = nil; // ToolBarButtonFont : QMUIToolbarButton 的字体
#pragma mark - SearchBar
- QMUICMI.searchBarTextFieldBackground = nil; // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色
+ QMUICMI.searchBarTextFieldBackgroundImage = nil; // SearchBarTextFieldBackgroundImage : QMUISearchBar 里的文本框的背景图,图片高度会决定输入框的高度
QMUICMI.searchBarTextFieldBorderColor = nil; // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色
- QMUICMI.searchBarBottomBorderColor = nil; // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色
- QMUICMI.searchBarBarTintColor = nil; // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色
+ QMUICMI.searchBarTextFieldCornerRadius = 2.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半
+ QMUICMI.searchBarBackgroundImage = nil; // SearchBarBackgroundImage : 搜索框的背景图,如果需要设置底部分隔线的颜色也请绘制到图片里
QMUICMI.searchBarTintColor = nil; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色
QMUICMI.searchBarTextColor = nil; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色
QMUICMI.searchBarPlaceholderColor = UIColorPlaceholder; // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色
QMUICMI.searchBarFont = nil; // SearchBarFont : QMUISearchBar 里的文本框的文字字体及 placeholder 的字体
QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon
QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片
- QMUICMI.searchBarTextFieldCornerRadius = 2.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半
- #pragma mark - TableView / TableViewCell
+ #pragma mark - Plain TableView
QMUICMI.tableViewEstimatedHeightEnabled = YES; // TableViewEstimatedHeightEnabled : 是否要开启全局 UITableView 的 estimatedRow(Section/Footer)Height
QMUICMI.tableViewBackgroundColor = nil; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色
- QMUICMI.tableViewGroupedBackgroundColor = nil; // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色
QMUICMI.tableSectionIndexColor = nil; // TableSectionIndexColor : 列表右边的字母索引条的文字颜色
QMUICMI.tableSectionIndexBackgroundColor = nil; // TableSectionIndexBackgroundColor : 列表右边的字母索引条的背景色
QMUICMI.tableSectionIndexTrackingBackgroundColor = nil; // TableSectionIndexTrackingBackgroundColor : 列表右边的字母索引条在选中时的背景色
@@ -152,7 +177,7 @@ - (void)applyConfigurationTemplate {
QMUICMI.tableViewCellNormalHeight = UITableViewAutomaticDimension; // TableViewCellNormalHeight : QMUITableView 的默认 cell 高度
QMUICMI.tableViewCellTitleLabelColor = nil; // TableViewCellTitleLabelColor : QMUITableViewCell 的 textLabel 的文字颜色
QMUICMI.tableViewCellDetailLabelColor = nil; // TableViewCellDetailLabelColor : QMUITableViewCell 的 detailTextLabel 的文字颜色
- QMUICMI.tableViewCellBackgroundColor = UIColorWhite; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色
+ QMUICMI.tableViewCellBackgroundColor = nil; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色
QMUICMI.tableViewCellSelectedBackgroundColor = UIColorMake(238, 239, 241); // TableViewCellSelectedBackgroundColor : QMUITableViewCell 点击时的背景色
QMUICMI.tableViewCellWarningBackgroundColor = UIColorYellow; // TableViewCellWarningBackgroundColor : QMUITableViewCell 用于表示警告时的背景色,备用
QMUICMI.tableViewCellDisclosureIndicatorImage = nil; // TableViewCellDisclosureIndicatorImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDisclosureIndicator 时的箭头的图片
@@ -170,7 +195,18 @@ - (void)applyConfigurationTemplate {
QMUICMI.tableViewSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionFooterAccessoryMargins : Plain 类型的 QMUITableView sectionFooter accessoryView 的间距
QMUICMI.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionHeaderContentInset : Plain 类型的 QMUITableView sectionHeader 里的内容的 padding
QMUICMI.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionFooterContentInset : Plain 类型的 QMUITableView sectionFooter 里的内容的 padding
-
+ if (@available(iOS 15, *)) {
+ QMUICMI.tableViewSectionHeaderTopPadding = UITableViewAutomaticDimension; // TableViewSectionHeaderTopPadding : Plain 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是22pt的空隙
+ }
+
+ #pragma mark - Grouped TableView
+ QMUICMI.tableViewGroupedBackgroundColor = nil; // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色
+ QMUICMI.tableViewGroupedSeparatorColor = TableViewSeparatorColor; // TableViewGroupedSeparatorColor : Grouped 类型的 QMUITableView 分隔线颜色
+ QMUICMI.tableViewGroupedCellTitleLabelColor = TableViewCellTitleLabelColor; // TableViewGroupedCellTitleLabelColor : Grouped 类型的 QMUITableView cell 里的标题颜色
+ QMUICMI.tableViewGroupedCellDetailLabelColor = TableViewCellDetailLabelColor; // TableViewGroupedCellDetailLabelColor : Grouped 类型的 QMUITableView cell 里的副标题颜色
+ QMUICMI.tableViewGroupedCellBackgroundColor = TableViewCellBackgroundColor; // TableViewGroupedCellBackgroundColor : Grouped 类型的 QMUITableView cell 背景色
+ QMUICMI.tableViewGroupedCellSelectedBackgroundColor = TableViewCellSelectedBackgroundColor; // TableViewGroupedCellSelectedBackgroundColor : Grouped 类型的 QMUITableView cell 点击时的背景色
+ QMUICMI.tableViewGroupedCellWarningBackgroundColor = TableViewCellWarningBackgroundColor; // tableViewGroupedCellWarningBackgroundColor : Grouped 类型的 QMUITableView cell 在提醒状态下的背景色
QMUICMI.tableViewGroupedSectionHeaderFont = UIFontMake(12); // TableViewGroupedSectionHeaderFont : Grouped 类型的 QMUITableView sectionHeader 里的文字字体
QMUICMI.tableViewGroupedSectionFooterFont = UIFontMake(12); // TableViewGroupedSectionFooterFont : Grouped 类型的 QMUITableView sectionFooter 里的文字字体
QMUICMI.tableViewGroupedSectionHeaderTextColor = UIColorGrayDarken; // TableViewGroupedSectionHeaderTextColor : Grouped 类型的 QMUITableView sectionHeader 里的文字颜色
@@ -181,43 +217,68 @@ - (void)applyConfigurationTemplate {
QMUICMI.tableViewGroupedSectionFooterDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionFooterDefaultHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN
QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, 15, 8, 15); // TableViewGroupedSectionHeaderContentInset : Grouped 类型的 QMUITableView sectionHeader 里的内容的 padding
QMUICMI.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, 15, 2, 15); // TableViewGroupedSectionFooterContentInset : Grouped 类型的 QMUITableView sectionFooter 里的内容的 padding
+ if (@available(iOS 15, *)) {
+ QMUICMI.tableViewGroupedSectionHeaderTopPadding = UITableViewAutomaticDimension; // TableViewGroupedSectionHeaderTopPadding : Grouped 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是0。
+ }
+
+ #pragma mark - InsetGrouped TableView
+ QMUICMI.tableViewInsetGroupedCornerRadius = 10; // TableViewInsetGroupedCornerRadius : InsetGrouped 类型的 UITableView 内 cell 的圆角值
+ QMUICMI.tableViewInsetGroupedHorizontalInset = PreferredValueForVisualDevice(20, 15); // TableViewInsetGroupedHorizontalInset: InsetGrouped 类型的 UITableView 内的左右缩进值
+ QMUICMI.tableViewInsetGroupedBackgroundColor = TableViewGroupedBackgroundColor; // TableViewInsetGroupedBackgroundColor : InsetGrouped 类型的 UITableView 的背景色
+ QMUICMI.tableViewInsetGroupedSeparatorColor = TableViewGroupedSeparatorColor; // TableViewInsetGroupedSeparatorColor : InsetGrouped 类型的 QMUITableView 分隔线颜色
+ QMUICMI.tableViewInsetGroupedCellTitleLabelColor = TableViewGroupedCellTitleLabelColor; // TableViewInsetGroupedCellTitleLabelColor : InsetGrouped 类型的 QMUITableView cell 里的标题颜色
+ QMUICMI.tableViewInsetGroupedCellDetailLabelColor = TableViewGroupedCellDetailLabelColor; // TableViewInsetGroupedCellDetailLabelColor : InsetGrouped 类型的 QMUITableView cell 里的副标题颜色
+ QMUICMI.tableViewInsetGroupedCellBackgroundColor = TableViewGroupedCellBackgroundColor; // TableViewInsetGroupedCellBackgroundColor : InsetGrouped 类型的 QMUITableView cell 背景色
+ QMUICMI.tableViewInsetGroupedCellSelectedBackgroundColor = TableViewGroupedCellSelectedBackgroundColor; // TableViewInsetGroupedCellSelectedBackgroundColor : InsetGrouped 类型的 QMUITableView cell 点击时的背景色
+ QMUICMI.tableViewInsetGroupedCellWarningBackgroundColor = TableViewGroupedCellWarningBackgroundColor; // TableViewInsetGroupedCellWarningBackgroundColor : InsetGrouped 类型的 QMUITableView cell 在提醒状态下的背景色
+ QMUICMI.tableViewInsetGroupedSectionHeaderFont = TableViewGroupedSectionHeaderFont; // TableViewInsetGroupedSectionHeaderFont : InsetGrouped 类型的 QMUITableView sectionHeader 里的文字字体
+ QMUICMI.tableViewInsetGroupedSectionFooterFont = TableViewInsetGroupedSectionHeaderFont; // TableViewInsetGroupedSectionFooterFont : InsetGrouped 类型的 QMUITableView sectionFooter 里的文字字体
+ QMUICMI.tableViewInsetGroupedSectionHeaderTextColor = TableViewGroupedSectionHeaderTextColor; // TableViewInsetGroupedSectionHeaderTextColor : InsetGrouped 类型的 QMUITableView sectionHeader 里的文字颜色
+ QMUICMI.tableViewInsetGroupedSectionFooterTextColor = TableViewInsetGroupedSectionHeaderTextColor; // TableViewInsetGroupedSectionFooterTextColor : InsetGrouped 类型的 QMUITableView sectionFooter 里的文字颜色
+ QMUICMI.tableViewInsetGroupedSectionHeaderAccessoryMargins = TableViewGroupedSectionHeaderAccessoryMargins; // TableViewInsetGroupedSectionHeaderAccessoryMargins : InsetGrouped 类型的 QMUITableView sectionHeader accessoryView 的间距
+ QMUICMI.tableViewInsetGroupedSectionFooterAccessoryMargins = TableViewInsetGroupedSectionHeaderAccessoryMargins; // TableViewInsetGroupedSectionFooterAccessoryMargins : InsetGrouped 类型的 QMUITableView sectionFooter accessoryView 的间距
+ QMUICMI.tableViewInsetGroupedSectionHeaderDefaultHeight = TableViewGroupedSectionHeaderDefaultHeight; // TableViewInsetGroupedSectionHeaderDefaultHeight : InsetGrouped 类型的 QMUITableView sectionHeader 的默认高度(也即没使用自定义的 sectionHeaderView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN
+ QMUICMI.tableViewInsetGroupedSectionFooterDefaultHeight = TableViewGroupedSectionFooterDefaultHeight; // TableViewInsetGroupedSectionFooterDefaultHeight : InsetGrouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN
+ QMUICMI.tableViewInsetGroupedSectionHeaderContentInset = TableViewGroupedSectionHeaderContentInset; // TableViewInsetGroupedSectionHeaderContentInset : InsetGrouped 类型的 QMUITableView sectionHeader 里的内容的 padding
+ QMUICMI.tableViewInsetGroupedSectionFooterContentInset = TableViewInsetGroupedSectionHeaderContentInset; // TableViewInsetGroupedSectionFooterContentInset : InsetGrouped 类型的 QMUITableView sectionFooter 里的内容的 padding
+ if (@available(iOS 15, *)) {
+ QMUICMI.tableViewInsetGroupedSectionHeaderTopPadding = UITableViewAutomaticDimension; // TableViewInsetGroupedSectionHeaderTopPadding : InsetGrouped 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是0。
+ }
#pragma mark - UIWindowLevel
QMUICMI.windowLevelQMUIAlertView = UIWindowLevelAlert - 4.0; // UIWindowLevelQMUIAlertView : QMUIModalPresentationViewController、QMUIPopupContainerView 里使用的 UIWindow 的 windowLevel
-
- #pragma mark - QMUILog
- QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log
- QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log
- QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelWarn 级别的 log
+ QMUICMI.windowLevelQMUIConsole = 1; // UIWindowLevelQMUIConsole : QMUIConsole 内部的 UIWindow 的 windowLevel
#pragma mark - QMUIBadge
- QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : UIBarButtoItem、UITabBarItem 上的未读数的背景色
- QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : UIBarButtoItem、UITabBarItem 上的未读数的文字颜色
- QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : UIBarButtoItem、UITabBarItem 上的未读数的字体
- QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : UIBarButtoItem、UITabBarItem 上的未读数与圆圈之间的 padding
- QMUICMI.badgeCenterOffset = CGPointMake(10, -9); // BadgeCenterOffset : UIBarButtoItem、UITabBarItem 上的未读数相对于 item 中心的偏移
- QMUICMI.badgeCenterOffsetLandscape = CGPointMake(10, -9); // BadgeCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读数在横屏下相对于 item 中心的偏移
+ QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : QMUIBadge 上的未读数的背景色
+ QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : QMUIBadge 上的未读数的文字颜色
+ QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : QMUIBadge 上的未读数的字体
+ QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : QMUIBadge 上的未读数与圆圈之间的 padding
+ QMUICMI.badgeOffset = CGPointMake(-9, 11); // BadgeOffset : QMUIBadge 上的未读数相对于目标 view 右上角的偏移
+ QMUICMI.badgeOffsetLandscape = CGPointMake(-9, 6); // BadgeOffsetLandscape : QMUIBadge 上的未读数在横屏下相对于目标 view 右上角的偏移
- QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : UIBarButtoItem、UITabBarItem 上的未读红点的颜色
- QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : UIBarButtoItem、UITabBarItem 上的未读红点的大小
- QMUICMI.updatesIndicatorCenterOffset = CGPointMake(10, -9); // UpdatesIndicatorCenterOffset : UIBarButtoItem、UITabBarItem 上的未读红点相对于 item 中心的偏移
- QMUICMI.updatesIndicatorCenterOffsetLandscape = CGPointMake(10, -9); // UpdatesIndicatorCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读红点在横屏下相对于 item 中心的偏移
+ QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : QMUIBadge 上的未读红点的颜色
+ QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : QMUIBadge 上的未读红点的大小
+ QMUICMI.updatesIndicatorOffset = CGPointMake(4, UpdatesIndicatorSize.height);// UpdatesIndicatorOffset : QMUIBadge 未读红点相对于目标 view 右上角的偏移
+ QMUICMI.updatesIndicatorOffsetLandscape = UpdatesIndicatorOffset; // UpdatesIndicatorOffsetLandscape : QMUIBadge 未读红点在横屏下相对于目标 view 右上角的偏移
#pragma mark - Others
QMUICMI.automaticCustomNavigationBarTransitionStyle = NO; // AutomaticCustomNavigationBarTransitionStyle : 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果
QMUICMI.supportedOrientationMask = UIInterfaceOrientationMaskAll; // SupportedOrientationMask : 默认支持的横竖屏方向
- QMUICMI.automaticallyRotateDeviceOrientation = NO; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕
- QMUICMI.statusbarStyleLightInitially = NO; // StatusbarStyleLightInitially : 默认的状态栏内容是否使用白色,默认为 NO,也即黑色
- QMUICMI.needsBackBarButtonItemTitle = NO; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image
+ QMUICMI.automaticallyRotateDeviceOrientation = NO; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕(仅 iOS 15 及以前版本需要,iOS 16 系统会自动处理,该开关无意义。)
+ QMUICMI.defaultStatusBarStyle = UIStatusBarStyleDefault; // DefaultStatusBarStyle : 默认的状态栏样式,默认值为 UIStatusBarStyleDefault,也即在 iOS 12 及以前是黑色文字,iOS 13 及以后会自动根据当前 App 是否处于 Dark Mode 切换颜色。如果你希望固定为白色,请设置为 UIStatusBarStyleLightContent,固定黑色则设置为 UIStatusBarStyleDarkContent。
+ QMUICMI.needsBackBarButtonItemTitle = YES; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image
QMUICMI.hidesBottomBarWhenPushedInitially = NO; // HidesBottomBarWhenPushedInitially : QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO
QMUICMI.preventConcurrentNavigationControllerTransitions = YES; // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash
QMUICMI.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO
- QMUICMI.shouldFixTabBarTransitionBugInIPhoneX = NO; // ShouldFixTabBarTransitionBugInIPhoneX : 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug
- QMUICMI.shouldFixTabBarButtonBugForAll = NO; // ShouldFixTabBarButtonBugForAll : 是否要对 iOS 12.1.1 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO
+ QMUICMI.shouldFixTabBarSafeAreaInsetsBug = NO; // ShouldFixTabBarSafeAreaInsetsBug : 是否要对 iOS 11 及以后的版本修复当存在 UITabBar 时,UIScrollView 的 inset.bottom 可能错误的 bug(issue #218 #934),默认为 YES
+ QMUICMI.shouldFixSearchBarMaskViewLayoutBug = NO; // ShouldFixSearchBarMaskViewLayoutBug : 是否自动修复 UISearchController.searchBar 被当作 tableHeaderView 使用时可能出现的布局 bug(issue #950)
QMUICMI.shouldPrintQMUIWarnLogToConsole = IS_DEBUG; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上
- QMUICMI.sendAnalyticsToQMUITeam = YES; // SendAnalyticsToQMUITeam : 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用
+ QMUICMI.dynamicPreferredValueForIPad = NO; // DynamicPreferredValueForIPad : 当 iPad 处于 Slide Over 或 Split View 分屏模式下,宏 `PreferredValueForXXX` 是否把 iPad 视为某种屏幕宽度近似的 iPhone 来取值。
+ QMUICMI.ignoreKVCAccessProhibited = NO; // IgnoreKVCAccessProhibited : 是否全局忽略 iOS 13 对 KVC 访问 UIKit 私有属性的限制
+ QMUICMI.adjustScrollIndicatorInsetsByContentInsetAdjustment = NO; // AdjustScrollIndicatorInsetsByContentInsetAdjustment : 当将 UIScrollView.contentInsetAdjustmentBehavior 设为 UIScrollViewContentInsetAdjustmentNever 时,是否自动将 UIScrollView.automaticallyAdjustsScrollIndicatorInsets 设为 NO,以保证原本在 iOS 12 下的代码不用修改就能在 iOS 13 下正常控制滚动条的位置。
}
// QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。
diff --git a/QMUI/QMUIKit.podspec b/QMUI/QMUIKit.podspec
index e27b76eb..aef548fd 100644
--- a/QMUI/QMUIKit.podspec
+++ b/QMUI/QMUIKit.podspec
@@ -1,35 +1,33 @@
Pod::Spec.new do |s|
s.name = "QMUIKit"
- s.version = "3.2.0"
+ s.version = "4.8.0"
s.summary = "致力于提高项目 UI 开发效率的解决方案"
s.description = <<-DESC
QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理, 让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。
DESC
- s.homepage = "https://qmuiteam.com/ios"
+ s.homepage = "https://github.com/Tencent/QMUI_iOS"
s.license = 'MIT'
s.author = {"qmuiteam" => "contact@qmuiteam.com"}
s.source = {:git => "https://github.com/Tencent/QMUI_iOS.git", :tag => s.version.to_s}
#s.source = {:git => "https://github.com/Tencent/QMUI_iOS.git", :branch => 'master'}
s.social_media_url = 'https://github.com/Tencent/QMUI_iOS'
s.requires_arc = true
- s.documentation_url = 'https://qmuiteam.com/ios/page/document.html'
+ s.documentation_url = 'https://github.com/Tencent/QMUI_iOS'
s.screenshot = 'https://cloud.githubusercontent.com/assets/1190261/26751376/63f96538-486a-11e7-81cf-5bc83a945207.png'
- s.platform = :ios, '9.0'
- s.frameworks = 'Foundation', 'UIKit', 'CoreGraphics', 'Photos'
+ s.platform = :ios, '13.0'
+ s.frameworks = 'Foundation', 'UIKit', 'CoreGraphics'
s.preserve_paths = 'QMUIConfigurationTemplate/*'
s.source_files = 'QMUIKit/QMUIKit.h'
+ s.resource_bundles = {'QMUIKit' => ['QMUIKit/PrivacyInfo.xcprivacy']}
s.subspec 'QMUICore' do |ss|
- ss.source_files = 'QMUIKit/QMUIKit.h', 'QMUIKit/QMUICore', 'QMUIKit/UIKitExtensions'
+ ss.source_files = 'QMUIKit/QMUIKit.h', 'QMUIKit/QMUICore', 'QMUIKit/UIKitExtensions', 'QMUIKit/UIKitExtensions/QMUIBarProtocol'
+ ss.frameworks = 'CoreImage', 'ImageIO'
ss.dependency 'QMUIKit/QMUIWeakObjectContainer'
ss.dependency 'QMUIKit/QMUILog'
end
- s.subspec 'QMUIResources' do |ss|
- ss.resource = 'QMUIKit/QMUIResources/*.bundle'
- end
-
s.subspec 'QMUIMainFrame' do |ss|
ss.source_files = 'QMUIKit/QMUIMainFrame'
ss.dependency 'QMUIKit/QMUICore'
@@ -42,6 +40,15 @@ Pod::Spec.new do |s|
ss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates'
end
+ s.subspec 'QMUIResources' do |ss|
+ ss.resource_bundles = {'QMUIResources' => ['QMUIKit/QMUIResources/*.*']}
+ ss.pod_target_xcconfig = {
+ 'EXPANDED_CODE_SIGN_IDENTITY' => '',
+ 'CODE_SIGNING_REQUIRED' => 'NO',
+ 'CODE_SIGNING_ALLOWED' => 'NO',
+ }
+ end
+
s.subspec 'QMUIWeakObjectContainer' do |ss|
ss.source_files = 'QMUIKit/QMUIComponents/QMUIWeakObjectContainer.{h,m}'
end
@@ -59,28 +66,24 @@ Pod::Spec.new do |s|
sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates'
end
- ss.subspec 'QMUINavigationTitleView' do |sss|
- sss.source_files = 'QMUIKit/QMUIComponents/QMUINavigationTitleView.{h,m}'
- sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
- end
-
- ss.subspec 'QMUIButton' do |sss|
- sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIButton.{h,m}'
+ ss.subspec 'QMUICALayerExtension' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.{h,m}'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates'
end
- ss.subspec 'QMUIFillButton' do |sss|
- sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.{h,m}'
- sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ ss.subspec 'QMUIAnimation' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUIAnimation'
end
- ss.subspec 'QMUIGhostButton' do |sss|
- sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.{h,m}'
+ ss.subspec 'QMUINavigationTitleView' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUINavigationTitleView.{h,m}'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
end
- ss.subspec 'QMUILinkButton' do |sss|
- sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.{h,m}'
- sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ ss.subspec 'QMUIButton' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIButton.{h,m}'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUILayouter'
end
ss.subspec 'QMUINavigationButton' do |sss|
@@ -104,14 +107,29 @@ Pod::Spec.new do |s|
ss.subspec 'QMUIEmptyView' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIEmptyView.{h,m}'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
end
ss.subspec 'QMUILabel' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUILabel.{h,m}'
end
+ ss.subspec 'QMUILayouter' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUILayouter/*.{h,m}'
+ end
+
+ ss.subspec 'QMUISheetPresentation' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUISheetPresentation/*.{h,m}'
+ sss.dependency 'QMUIKit/QMUIMainFrame'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUINavigationButton'
+ end
+
ss.subspec 'QMUIKeyboardManager' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIKeyboardManager.{h,m}'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates'
end
# 从这里开始就是非必须的组件
@@ -125,6 +143,13 @@ Pod::Spec.new do |s|
sss.dependency 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
sss.dependency 'QMUIKit/QMUIComponents/QMUITextField'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIKeyboardManager'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUILabel'
+ end
+
+ ss.subspec 'QMUIAppearance' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUIAppearance.{h,m}'
end
ss.subspec 'QMUICellHeightCache' do |sss|
@@ -145,9 +170,13 @@ Pod::Spec.new do |s|
ss.subspec 'QMUIConsole' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIConsole/*.{h,m}'
+ sss.dependency 'QMUIKit/QMUIResources'
sss.dependency 'QMUIKit/QMUIComponents/QMUITextView'
sss.dependency 'QMUIKit/QMUIComponents/QMUITextField'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUITableView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewCell'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUICellHeightKeyCache'
sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupMenuView'
sss.dependency 'QMUIKit/QMUIComponents/QMUICAAnimationExtension'
end
@@ -166,12 +195,13 @@ Pod::Spec.new do |s|
sss.dependency 'QMUIKit/QMUIComponents/QMUITextField'
sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewCell'
sss.dependency 'QMUIKit/QMUIComponents/QMUINavigationTitleView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
end
ss.subspec 'QMUIEmotionView' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIEmotionView.{h,m}'
- sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
sss.dependency 'QMUIKit/QMUIResources'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
end
ss.subspec 'QMUIFloatLayoutView' do |sss|
@@ -189,6 +219,7 @@ Pod::Spec.new do |s|
sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView'
sss.dependency 'QMUIKit/QMUIComponents/QMUIPieProgressView'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
sss.dependency 'QMUIKit/QMUIMainFrame'
end
@@ -199,12 +230,14 @@ Pod::Spec.new do |s|
ss.subspec 'QMUIModalPresentationViewController' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController.{h,m}'
sss.dependency 'QMUIKit/QMUIComponents/QMUIKeyboardManager'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
end
ss.subspec 'QMUIMoreOperationController' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIMoreOperationController.{h,m}'
sss.dependency 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
end
ss.subspec 'QMUIOrderedDictionary' do |sss|
@@ -218,12 +251,17 @@ Pod::Spec.new do |s|
ss.subspec 'QMUIPopupContainerView' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIPopupContainerView.{h,m}'
sss.dependency 'QMUIKit/QMUIMainFrame'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
end
ss.subspec 'QMUIPopupMenuView' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIPopupMenuView/*.{h,m}'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUITableView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUILabel'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUILayouter'
sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupContainerView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUICheckbox'
end
ss.subspec 'QMUIScrollAnimator' do |sss|
@@ -251,10 +289,6 @@ Pod::Spec.new do |s|
sss.source_files = 'QMUIKit/QMUIComponents/QMUISegmentedControl.{h,m}'
end
- ss.subspec 'QMUISlider' do |sss|
- sss.source_files = 'QMUIKit/QMUIComponents/QMUISlider.{h,m}'
- end
-
ss.subspec 'QMUITableViewCell' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUITableViewCell.{h,m}'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
@@ -279,27 +313,49 @@ Pod::Spec.new do |s|
sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates'
end
+ ss.subspec 'QMUITheme' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUITheme/*.{h,m}'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIImagePickerLibrary'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAlertController'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIConsole'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIEmotionView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIGridView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIImagePreviewView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUILabel'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupContainerView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupMenuView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUITextField'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUITextView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIToastView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIBadge'
+ end
+
ss.subspec 'QMUITips' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUITips.{h,m}'
- sss.dependency 'QMUIKit/QMUIComponents/QMUIToastView'
sss.dependency 'QMUIKit/QMUIResources'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIToastView'
end
- ss.subspec 'QMUIVisualEffectView' do |sss|
- sss.source_files = 'QMUIKit/QMUIComponents/QMUIVisualEffectView.{h,m}'
+ ss.subspec 'QMUIWindowSizeMonitor' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.{h,m}'
end
ss.subspec 'QMUIZoomImageView' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIZoomImageView.{h,m}'
+ sss.frameworks = 'PhotosUI', 'CoreMedia', 'AVFoundation', 'QuartzCore'
+ sss.dependency 'QMUIKit/QMUIResources'
sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
- sss.dependency 'QMUIKit/QMUIComponents/QMUISlider'
sss.dependency 'QMUIKit/QMUIComponents/QMUIPieProgressView'
sss.dependency 'QMUIKit/QMUIComponents/QMUIAssetLibrary'
end
ss.subspec 'QMUIAssetLibrary' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/AssetLibrary/*.{h,m}'
+ sss.frameworks = 'Photos', 'CoreServices'
end
ss.subspec 'QMUIImagePickerLibrary' do |sss|
@@ -314,6 +370,7 @@ Pod::Spec.new do |s|
sss.dependency 'QMUIKit/QMUIComponents/QMUIZoomImageView'
sss.dependency 'QMUIKit/QMUIComponents/QMUIAlertController'
sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance'
end
@@ -344,7 +401,6 @@ Pod::Spec.new do |s|
ss.subspec 'QMUIToastView' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/ToastView/*.{h,m}'
sss.dependency 'QMUIKit/QMUIComponents/QMUIKeyboardManager'
- sss.dependency 'QMUIKit/QMUIComponents/QMUIVisualEffectView'
end
ss.subspec 'QMUIStaticTableView' do |sss|
@@ -353,6 +409,12 @@ Pod::Spec.new do |s|
sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates'
end
+ ss.subspec 'QMUICheckbox' do |sss|
+ sss.source_files = 'QMUIKit/QMUIComponents/QMUICheckbox.{h,m}'
+ sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
+ sss.dependency 'QMUIKit/QMUIResources'
+ end
+
end
end
diff --git a/QMUI/QMUIKit/Info.plist b/QMUI/QMUIKit/Info.plist
index 957c532e..ec0cc7b0 100644
--- a/QMUI/QMUIKit/Info.plist
+++ b/QMUI/QMUIKit/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 3.2.0
+ $(MARKETING_VERSION)
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPrincipalClass
diff --git a/QMUI/QMUIKit/PrivacyInfo.xcprivacy b/QMUI/QMUIKit/PrivacyInfo.xcprivacy
new file mode 100644
index 00000000..0734d308
--- /dev/null
+++ b/QMUI/QMUIKit/PrivacyInfo.xcprivacy
@@ -0,0 +1,23 @@
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryUserDefaults
+ NSPrivacyAccessedAPITypeReasons
+
+ CA92.1
+
+
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTrackingDomains
+
+ NSPrivacyTracking
+
+
+
diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h
index 67f09886..9277b810 100644
--- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h
+++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAsset.h
@@ -43,7 +43,7 @@ typedef NS_ENUM(NSUInteger, QMUIAssetDownloadStatus) {
/**
* 相册里某一个资源的包装对象,该资源可能是图片、视频等。
- * @note QMUIAsset 重写了 isEqual: 方法,只要两个 QMUIAsset 的 adentifier 相同,则认为是同一个对象,以方便在数组、字典等容器中对大量 QMUIAsset 进行遍历查找等操作。
+ * @note QMUIAsset 重写了 isEqual: 方法,只要两个 QMUIAsset 的 identifier 相同,则认为是同一个对象,以方便在数组、字典等容器中对大量 QMUIAsset 进行遍历查找等操作。
*/
@interface QMUIAsset : NSObject
@@ -117,11 +117,11 @@ typedef NS_ENUM(NSUInteger, QMUIAssetDownloadStatus) {
* @param completion 完成请求后调用的 block,参数中包含了请求的 Live Photo 以及相关信息,若 assetType 不是 QMUIAssetTypeLivePhoto 则为 nil
* @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。
*
- * @wraning iOS 9.1 以下中并没有 Live Photo,因此无法获取有效结果。
+ * @warning iOS 9.1 以下中并没有 Live Photo,因此无法获取有效结果。
*
* @return 返回请求图片的请求 id
*/
-- (NSInteger)requestLivePhotoWithCompletion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler NS_AVAILABLE_IOS(9_1);
+- (NSInteger)requestLivePhotoWithCompletion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler;
/**
* 异步请求 AVPlayerItem,可能会有网络请求
diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m
index 9f04afcc..0f6b6b7e 100644
--- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m
+++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAsset.m
@@ -15,7 +15,7 @@
#import "QMUIAsset.h"
#import
-#import
+#import
#import "QMUICore.h"
#import "QMUIAssetsManager.h"
#import "NSString+QMUI.h"
@@ -42,15 +42,11 @@ - (instancetype)initWithPHAsset:(PHAsset *)phAsset {
switch (phAsset.mediaType) {
case PHAssetMediaTypeImage:
_assetType = QMUIAssetTypeImage;
- if ([[phAsset valueForKey:@"uniformTypeIdentifier"] isEqualToString:(__bridge NSString *)kUTTypeGIF]) {
+ if ([[phAsset qmui_valueForKey:@"uniformTypeIdentifier"] isEqualToString:(__bridge NSString *)kUTTypeGIF]) {
_assetSubType = QMUIAssetSubTypeGIF;
} else {
- if (@available(iOS 9.1, *)) {
- if (phAsset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) {
- _assetSubType = QMUIAssetSubTypeLivePhoto;
- } else {
- _assetSubType = QMUIAssetSubTypeImage;
- }
+ if (phAsset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) {
+ _assetSubType = QMUIAssetSubTypeLivePhoto;
} else {
_assetSubType = QMUIAssetSubTypeImage;
}
diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h
index c0d1ddf0..d4e236b6 100644
--- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h
+++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAssetsGroup.h
diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m
index 999c42c6..528916b4 100644
--- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m
+++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAssetsGroup.m
@@ -30,8 +30,7 @@ @implementation QMUIAssetsGroup
- (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection fetchAssetsOptions:(PHFetchOptions *)pHFetchOptions {
self = [super init];
if (self) {
- PHFetchResult *phFetchResult = [PHAsset fetchAssetsInAssetCollection:phAssetCollection options:pHFetchOptions];
- self.phFetchResult = phFetchResult;
+ self.phFetchResult = [PHAsset fetchAssetsInAssetCollection:phAssetCollection options:pHFetchOptions];
self.phAssetCollection = phAssetCollection;
}
return self;
diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h
index 95e8b489..178da093 100644
--- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h
+++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAssetsManager.h
@@ -122,7 +122,7 @@ extern void QMUISaveVideoAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(NSString *
*
* @return 返回包含所有合适相册的数组
*/
-+ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum;
++ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum;
/// 获取一个 PHAssetCollection 中创建日期最新的资源
+ (PHAsset *)fetchLatestAssetWithAssetCollection:(PHAssetCollection *)assetCollection;
diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m
index f9f7c4be..ff0cf089 100644
--- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m
+++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAssetsManager.m
@@ -91,14 +91,14 @@ + (void)requestAuthorization:(void(^)(QMUIAssetAuthorizationStatus status))handl
- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbumIfSupported:(BOOL)showSmartAlbumIfSupported usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock {
// 根据条件获取所有合适的相册,并保存到临时数组中
- NSArray *tempAlbumsArray = [PHPhotoLibrary fetchAllAlbumsWithAlbumContentType:contentType showEmptyAlbum:showEmptyAlbum showSmartAlbum:showSmartAlbumIfSupported];
+ NSArray *tempAlbumsArray = [PHPhotoLibrary fetchAllAlbumsWithAlbumContentType:contentType showEmptyAlbum:showEmptyAlbum showSmartAlbum:showSmartAlbumIfSupported];
// 创建一个 PHFetchOptions,用于 QMUIAssetsGroup 对资源的排序以及对内容类型进行控制
PHFetchOptions *phFetchOptions = [PHPhotoLibrary createFetchOptionsWithAlbumContentType:contentType];
// 遍历结果,生成对应的 QMUIAssetsGroup,并调用 enumerationBlock
- for (NSUInteger i = 0; i < [tempAlbumsArray count]; i++) {
- PHAssetCollection *phAssetCollection = [tempAlbumsArray objectAtIndex:i];
+ for (NSUInteger i = 0; i < tempAlbumsArray.count; i++) {
+ PHAssetCollection *phAssetCollection = tempAlbumsArray[i];
QMUIAssetsGroup *assetsGroup = [[QMUIAssetsGroup alloc] initWithPHCollection:phAssetCollection fetchAssetsOptions:phFetchOptions];
if (enumerationBlock) {
enumerationBlock(assetsGroup);
@@ -212,8 +212,8 @@ + (PHFetchOptions *)createFetchOptionsWithAlbumContentType:(QMUIAlbumContentType
return fetchOptions;
}
-+ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum {
- NSMutableArray *tempAlbumsArray = [[NSMutableArray alloc] init];
++ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum {
+ NSMutableArray *tempAlbumsArray = [[NSMutableArray alloc] init];
// 创建一个 PHFetchOptions,用于创建 QMUIAssetsGroup 对资源的排序和类型进行控制
PHFetchOptions *fetchOptions = [PHPhotoLibrary createFetchOptionsWithAlbumContentType:contentType];
@@ -284,7 +284,7 @@ + (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentTyp
}
}
- NSArray *resultAlbumsArray = [tempAlbumsArray copy];
+ NSArray *resultAlbumsArray = [tempAlbumsArray copy];
return resultAlbumsArray;
}
diff --git a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h
index f331845e..ec7f73fe 100644
--- a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h
+++ b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// CAAnimation+QMUI.h
@@ -15,6 +15,8 @@
#import
+// 这个文件依赖了 QMUIMultipleDelegates,无法作为 UIKitExtensions 的一部分,所以放在 QMUIComponents 内
+
@interface CAAnimation (QMUI)
@property(nonatomic, copy) void (^qmui_animationDidStartBlock)(__kindof CAAnimation *aAnimation);
diff --git a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m
index 6bf6cfc2..9a1e5831 100644
--- a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m
+++ b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// CAAnimation+QMUI.m
diff --git a/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.h b/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.h
new file mode 100644
index 00000000..09c2773d
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.h
@@ -0,0 +1,34 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// CALayer+QMUIViewAnimation.h
+// QMUIKit
+//
+// Created by ziezheng on 2020/4/4.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface CALayer (QMUIViewAnimation)
+
+/**
+ 开启了该属性的 CALayer 可在 +[UIView animateWithDuration:animations:] 执行动画,系统默认是不支持这种做法的。
+
+ @code
+ [UIView animateWithDuration:1 animations:^{
+ layer.frame = xxx;
+ } completion:nil];
+ @endcode
+ */
+@property(nonatomic, assign) BOOL qmui_viewAnimationEnabled;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.m b/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.m
new file mode 100644
index 00000000..0c1dcea1
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.m
@@ -0,0 +1,98 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// CALayer+QMUIViewAnimation.m
+// QMUIKit
+//
+// Created by ziezheng on 2020/4/4.
+//
+
+#import "CALayer+QMUIViewAnimation.h"
+#import "CALayer+QMUI.h"
+#import "QMUICore.h"
+#import "QMUIMultipleDelegates.h"
+
+
+@interface _QMUICALayerDelegator : NSObject
+
+@end
+
+@implementation _QMUICALayerDelegator
+
++ (instancetype)sharedDelegator {
+ static dispatch_once_t onceToken;
+ static _QMUICALayerDelegator *instance = nil;
+ dispatch_once(&onceToken,^{
+ instance = [[super allocWithZone:NULL] init];
+ });
+ return instance;
+}
+
++ (id)allocWithZone:(struct _NSZone *)zone {
+ return [self sharedDelegator];
+}
+
+- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
+ static UIView *standardView = nil;
+ if (!standardView) standardView = UIView.new;
+ // 被 +[UIView animateWithDuration:animations:] 包裹的代码可利用任意 UIView 的 actionForLayer:forKey: 来获得默认的 CAAction
+ id action = [standardView actionForLayer:standardView.layer forKey:event];
+ if (action == [NSNull null]) {
+ // -[CALayer actionForKey:] 会先询问本代理,一旦代理返回了 NSNull, 则不会执行 self.actions 里隐式动画,为保持 CALayer 的原有逻辑,这里返回 nil,详见 -[CALayer actionForKey:] 的文档描述。
+ return nil;
+ } else {
+ return action;
+ }
+}
+
+@end
+
+@implementation CALayer (QMUIViewAnimation)
+
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ OverrideImplementation([CALayer class], @selector(addAnimation:forKey:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(CALayer *selfObject, CAAnimation *animation, NSString *key) {
+ if (selfObject.qmui_viewAnimationEnabled) {
+ BOOL isViewAnimtion = [animation isKindOfClass:CABasicAnimation.class] && [animation.delegate isKindOfClass:NSClassFromString(@"UIViewAnimationState")];
+ if (isViewAnimtion) {
+ // 这里需要清空 fromValue 和 toValue,后面会在 CAMediaTimingCopyRenderTiming 取到这个 animtion 的参数并设置到 CATransaction 中,让 Layer 改变属性时,运用上这些动画
+ ((CABasicAnimation *)animation).fromValue = nil;
+ ((CABasicAnimation *)animation).toValue = nil;
+ // 这个机制下的 toValue 已是最终值,这里 additive 要设置成 NO,否则会多叠加一次计算结果,导致动画出错。
+ ((CABasicAnimation *)animation).additive = NO;
+ }
+ }
+ void (*originSelectorIMP)(id, SEL, CAAnimation *, NSString *);
+ originSelectorIMP = (void (*)(id, SEL, CAAnimation *, NSString *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, animation, key);
+
+ };
+ });
+ });
+}
+
+
+static char kAssociatedObjectKey_qmuiviewAnimationEnabled;
+- (void)setQmui_viewAnimationEnabled:(BOOL)qmui_viewAnimationEnabled {
+ QMUIAssert(!self.qmui_isRootLayerOfView, @"CALayer (QMUIViewAnimation)", @"UIView 本身的 Layer 无须开启 %s", __func__);
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiviewAnimationEnabled, @(qmui_viewAnimationEnabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (qmui_viewAnimationEnabled) {
+ self.qmui_multipleDelegatesEnabled = YES;
+ self.delegate = [_QMUICALayerDelegator sharedDelegator];
+ } else {
+ [self qmui_removeDelegate:[_QMUICALayerDelegator sharedDelegator]];
+ }
+}
+
+- (BOOL)qmui_viewAnimationEnabled {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiviewAnimationEnabled)) boolValue];
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h
index ad06810e..aef27cd3 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAlbumViewController.h
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m
index 4124fc9e..201b7c08 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAlbumViewController.m
@@ -20,6 +20,7 @@
#import "QMUIAssetsManager.h"
#import "QMUIImagePickerViewController.h"
#import "QMUIImagePickerHelper.h"
+#import "QMUIAppearance.h"
#import
#import
#import
@@ -45,13 +46,8 @@ + (void)initialize {
- (void)didInitializeWithStyle:(UITableViewCellStyle)style {
[super didInitializeWithStyle:style];
- self.albumImageSize = [QMUIAlbumTableViewCell appearance].albumImageSize;
- self.albumImageMarginLeft = [QMUIAlbumTableViewCell appearance].albumImageMarginLeft;
- self.albumNameInsets = [QMUIAlbumTableViewCell appearance].albumNameInsets;
- self.albumNameFont = [QMUIAlbumTableViewCell appearance].albumNameFont;
- self.albumNameColor = [QMUIAlbumTableViewCell appearance].albumNameColor;
- self.albumAssetsNumberFont = [QMUIAlbumTableViewCell appearance].albumAssetsNumberFont;
- self.albumAssetsNumberColor = [QMUIAlbumTableViewCell appearance].albumAssetsNumberColor;
+
+ [self qmui_applyAppearance];
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.clipsToBounds = YES;
@@ -109,23 +105,19 @@ - (void)setAlbumAssetsNumberColor:(UIColor *)albumAssetsNumberColor {
@implementation QMUIAlbumViewController (UIAppearance)
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
+}
+
+ (void)initialize {
- static dispatch_once_t onceToken1;
- dispatch_once(&onceToken1, ^{
- [self appearance]; // +initialize 时就先设置好默认样式
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ [self initAppearance];
});
}
-static QMUIAlbumViewController *albumViewControllerAppearance;
-+ (nonnull instancetype)appearance {
- static dispatch_once_t onceToken2;
- dispatch_once(&onceToken2, ^{
- if (!albumViewControllerAppearance) {
- albumViewControllerAppearance = [[QMUIAlbumViewController alloc] init];
- albumViewControllerAppearance.albumTableViewCellHeight = 88;
- }
- });
- return albumViewControllerAppearance;
++ (void)initAppearance {
+ QMUIAlbumViewController.appearance.albumTableViewCellHeight = 88;
}
@end
@@ -144,10 +136,7 @@ @implementation QMUIAlbumViewController
- (void)didInitialize {
[super didInitialize];
_shouldShowDefaultLoadingView = YES;
- if (albumViewControllerAppearance) {
- // 避免 albumViewControllerAppearance init 时走到这里来,导致死循环
- self.albumTableViewCellHeight = [QMUIAlbumViewController appearance].albumTableViewCellHeight;
- }
+ [self qmui_applyAppearance];
}
- (void)setupNavigationItems {
@@ -186,23 +175,37 @@ - (void)viewDidLoad {
if (self.shouldShowDefaultLoadingView) {
[self showEmptyViewWithLoading];
}
- dispatch_async(dispatch_get_global_queue(0, 0), ^{
- __weak __typeof(self)weakSelf = self;
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[QMUIAssetsManager sharedInstance] enumerateAllAlbumsWithAlbumContentType:self.contentType usingBlock:^(QMUIAssetsGroup *resultAssetsGroup) {
- dispatch_async(dispatch_get_main_queue(), ^{
- // 这里需要对 UI 进行操作,因此放回主线程处理
- __strong __typeof(weakSelf)strongSelf = weakSelf;
- if (resultAssetsGroup) {
- [strongSelf.albumsArray addObject:resultAssetsGroup];
- } else {
- [strongSelf refreshAlbumAndShowEmptyTipIfNeed];
- }
- });
+ if (resultAssetsGroup) {
+ [self.albumsArray addObject:resultAssetsGroup];
+ } else {
+ // 意味着遍历完所有的相簿了
+ [self sortAlbumArray];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self refreshAlbumAndShowEmptyTipIfNeed];
+ });
+ }
}];
});
}
}
+- (void)sortAlbumArray {
+ // 把隐藏相册排序强制放到最后
+ __block QMUIAssetsGroup *hiddenGroup = nil;
+ [self.albumsArray enumerateObjectsUsingBlock:^(QMUIAssetsGroup * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ if (obj.phAssetCollection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumAllHidden) {
+ hiddenGroup = obj;
+ *stop = YES;
+ }
+ }];
+ if (hiddenGroup) {
+ [self.albumsArray removeObject:hiddenGroup];
+ [self.albumsArray addObject:hiddenGroup];
+ }
+}
+
- (void)refreshAlbumAndShowEmptyTipIfNeed {
if ([self.albumsArray count] > 0) {
if ([self.albumViewControllerDelegate respondsToSelector:@selector(albumViewControllerWillFinishLoading:)]) {
@@ -224,7 +227,7 @@ - (void)pickAlbumsGroup:(QMUIAssetsGroup *)assetsGroup animated:(BOOL)animated {
if (!self.imagePickerViewController) {
self.imagePickerViewController = [self.albumViewControllerDelegate imagePickerViewControllerForAlbumViewController:self];
}
- NSAssert(self.imagePickerViewController, @"self.%@ 必须实现 %@ 并返回一个 %@ 对象", NSStringFromSelector(@selector(albumViewControllerDelegate)), NSStringFromSelector(@selector(imagePickerViewControllerForAlbumViewController:)), NSStringFromClass([QMUIImagePickerViewController class]));
+ QMUIAssert(!!self.imagePickerViewController, NSStringFromClass(self.class), NSStringFromClass(self.class), @"self.%@ 必须实现 %@ 并返回一个 %@ 对象", NSStringFromSelector(@selector(albumViewControllerDelegate)), NSStringFromSelector(@selector(imagePickerViewControllerForAlbumViewController:)), NSStringFromClass([QMUIImagePickerViewController class]));
[self.imagePickerViewController refreshWithAssetsGroup:assetsGroup];
self.imagePickerViewController.title = [assetsGroup name];
@@ -253,12 +256,9 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
cell = [[QMUIAlbumTableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellIdentifer];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
- QMUIAssetsGroup *assetsGroup = [self.albumsArray objectAtIndex:indexPath.row];
- // 显示相册缩略图
+ QMUIAssetsGroup *assetsGroup = self.albumsArray[indexPath.row];
cell.imageView.image = [assetsGroup posterImageWithSize:CGSizeMake(self.albumTableViewCellHeight, self.albumTableViewCellHeight)];
- // 显示相册名称
cell.textLabel.text = [assetsGroup name];
- // 显示相册中所包含的资源数量
cell.detailTextLabel.text = [NSString stringWithFormat:@"· %@", @(assetsGroup.numberOfAssets)];
[cell updateCellAppearanceWithIndexPath:indexPath];
return cell;
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h
index 79f576c8..55a1e61b 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePickerCollectionViewCell.h
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m
index 698e3075..44404cd9 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePickerCollectionViewCell.m
@@ -23,6 +23,7 @@
#import "QMUIButton.h"
#import "UIView+QMUI.h"
#import "NSString+QMUI.h"
+#import "QMUIAppearance.h"
@interface QMUIImagePickerCollectionViewCell ()
@@ -55,14 +56,7 @@ - (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initImagePickerCollectionViewCellUI];
- self.favoriteImage = [QMUIImagePickerCollectionViewCell appearance].favoriteImage;
- self.favoriteImageMargins = [QMUIImagePickerCollectionViewCell appearance].favoriteImageMargins;
- self.checkboxImage = [QMUIImagePickerCollectionViewCell appearance].checkboxImage;
- self.checkboxCheckedImage = [QMUIImagePickerCollectionViewCell appearance].checkboxCheckedImage;
- self.checkboxButtonMargins = [QMUIImagePickerCollectionViewCell appearance].checkboxButtonMargins;
- self.videoDurationLabelFont = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelFont;
- self.videoDurationLabelTextColor = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelTextColor;
- self.videoDurationLabelMargins = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelMargins;
+ [self qmui_applyAppearance];
}
return self;
}
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h
index 2247b1d5..8ee0bfa1 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePickerHelper.h
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m
index e44017b0..f932d96d 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePickerHelper.m
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h
index a3995b38..b9315298 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePickerPreviewViewController.h
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m
index 5b15b3d7..68866778 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePickerPreviewViewController.m
@@ -27,31 +27,27 @@
#import "QMUIAlertController.h"
#import "UIImage+QMUI.h"
#import "UIView+QMUI.h"
-#import "UIControl+QMUI.h"
#import "QMUILog.h"
+#import "QMUIAppearance.h"
#pragma mark - QMUIImagePickerPreviewViewController (UIAppearance)
@implementation QMUIImagePickerPreviewViewController (UIAppearance)
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
+}
+
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- [self appearance]; // +initialize 时就先设置好默认样式
+ [self initAppearance];
});
}
-static QMUIImagePickerPreviewViewController *imagePickerPreviewViewControllerAppearance;
-+ (nonnull instancetype)appearance {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- if (!imagePickerPreviewViewControllerAppearance) {
- imagePickerPreviewViewControllerAppearance = [[QMUIImagePickerPreviewViewController alloc] init];
- imagePickerPreviewViewControllerAppearance.toolBarBackgroundColor = UIColorMakeWithRGBA(27, 27, 27, .9f);
- imagePickerPreviewViewControllerAppearance.toolBarTintColor = UIColorWhite;
- }
- });
- return imagePickerPreviewViewControllerAppearance;
++ (void)initAppearance {
+ QMUIImagePickerPreviewViewController.appearance.toolBarBackgroundColor = UIColorMakeWithRGBA(27, 27, 27, .9f);
+ QMUIImagePickerPreviewViewController.appearance.toolBarTintColor = UIColorWhite;
}
@end
@@ -64,11 +60,8 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
self.maximumSelectImageCount = INT_MAX;
self.minimumSelectImageCount = 0;
- if (imagePickerPreviewViewControllerAppearance) {
- // 避免 imagePickerPreviewViewControllerAppearance init 时走到这里来,导致死循环
- self.toolBarBackgroundColor = [QMUIImagePickerPreviewViewController appearance].toolBarBackgroundColor;
- self.toolBarTintColor = [QMUIImagePickerPreviewViewController appearance].toolBarTintColor;
- }
+
+ [self qmui_applyAppearance];
}
return self;
}
@@ -109,10 +102,6 @@ - (void)viewWillAppear:(BOOL)animated {
self.checkboxButton.selected = [self.selectedImageAssetArray containsObject:imageAsset];
}
- BeginIgnoreDeprecatedWarning
- [[UIApplication sharedApplication] setStatusBarHidden:YES];
- EndIgnoreDeprecatedWarning
-
if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) {
UIViewController *vc = (UIViewController *)self;
if ([vc respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfHideable)] &&
@@ -126,10 +115,6 @@ - (void)viewWillAppear:(BOOL)animated {
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
- BeginIgnoreDeprecatedWarning
- [[UIApplication sharedApplication] setStatusBarHidden:NO];
- EndIgnoreDeprecatedWarning
-
if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) {
UIViewController *vc = (UIViewController *)self;
if ([vc respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfHideable)] &&
@@ -145,9 +130,9 @@ - (void)viewDidLayoutSubviews {
self.topToolBarView.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), NavigationContentTopConstant);
CGFloat topToolbarPaddingTop = SafeAreaInsetsConstantForDeviceWithNotch.top;
CGFloat topToolbarContentHeight = CGRectGetHeight(self.topToolBarView.bounds) - topToolbarPaddingTop;
- self.backButton.frame = CGRectSetXY(self.backButton.frame, 16 + self.view.qmui_safeAreaInsets.left, topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.backButton.frame)));
+ self.backButton.frame = CGRectSetXY(self.backButton.frame, 16 + self.view.safeAreaInsets.left, topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.backButton.frame)));
if (!self.checkboxButton.hidden) {
- self.checkboxButton.frame = CGRectSetXY(self.checkboxButton.frame, CGRectGetWidth(self.topToolBarView.frame) - 10 - self.view.qmui_safeAreaInsets.right - CGRectGetWidth(self.checkboxButton.frame), topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.checkboxButton.frame)));
+ self.checkboxButton.frame = CGRectSetXY(self.checkboxButton.frame, CGRectGetWidth(self.topToolBarView.frame) - 10 - self.view.safeAreaInsets.right - CGRectGetWidth(self.checkboxButton.frame), topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.checkboxButton.frame)));
}
}
@@ -198,10 +183,8 @@ - (NSUInteger)numberOfImagesInImagePreviewView:(QMUIImagePreviewView *)imagePrev
- (QMUIImagePreviewMediaType)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView assetTypeAtIndex:(NSUInteger)index {
QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:index];
if (imageAsset.assetType == QMUIAssetTypeImage) {
- if (@available(iOS 9.1, *)) {
- if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) {
- return QMUIImagePreviewMediaTypeLivePhoto;
- }
+ if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) {
+ return QMUIImagePreviewMediaTypeLivePhoto;
}
return QMUIImagePreviewMediaTypeImage;
} else if (imageAsset.assetType == QMUIAssetTypeVideo) {
@@ -355,13 +338,8 @@ - (void)requestImageForZoomImageView:(QMUIZoomImageView *)zoomImageView withInde
// 这么写是为了消除 Xcode 的 API available warning
BOOL isLivePhoto = NO;
- if (@available(iOS 9.1, *)) {
- if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) {
- isLivePhoto = YES;
- }
- }
-
- if (isLivePhoto) {
+ if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) {
+ isLivePhoto = YES;
imageView.tag = -1;
imageAsset.requestID = [imageAsset requestLivePhotoWithCompletion:^void(PHLivePhoto *livePhoto, NSDictionary *info) {
// 这里可能因为 imageView 复用,导致前面的请求得到的结果显示到别的 imageView 上,
@@ -373,9 +351,7 @@ - (void)requestImageForZoomImageView:(QMUIZoomImageView *)zoomImageView withInde
if (!loadICloudImageFault && (isNewRequest || isCurrentRequest)) {
// 如果是走 PhotoKit 的逻辑,那么这个 block 会被多次调用,并且第一次调用时返回的图片是一张小图,
// 这时需要把图片放大到跟屏幕一样大,避免后面加载大图后图片的显示会有跳动
- if (@available(iOS 9.1, *)) {
- imageView.livePhoto = livePhoto;
- }
+ imageView.livePhoto = livePhoto;
}
BOOL downloadSucceed = (livePhoto && !info) || (![[info objectForKey:PHLivePhotoInfoCancelledKey] boolValue] && ![info objectForKey:PHLivePhotoInfoErrorKey] && ![[info objectForKey:PHLivePhotoInfoIsDegradedKey] boolValue]);
if (downloadSucceed) {
@@ -392,6 +368,9 @@ - (void)requestImageForZoomImageView:(QMUIZoomImageView *)zoomImageView withInde
});
} withProgressHandler:phProgressHandler];
imageView.tag = imageAsset.requestID;
+ }
+
+ if (isLivePhoto) {
} else if (imageAsset.assetSubType == QMUIAssetSubTypeGIF) {
[imageAsset requestImageData:^(NSData *imageData, NSDictionary *info, BOOL isGIF, BOOL isHEIC) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h
index 1cc2a1ff..bd0082b7 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePickerViewController.h
diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m
index 04796fa2..d1bd78c5 100644
--- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m
+++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePickerViewController.m
@@ -28,9 +28,9 @@
#import "UIView+QMUI.h"
#import
#import "QMUIEmptyView.h"
-#import "UIControl+QMUI.h"
#import "UIViewController+QMUI.h"
#import "QMUILog.h"
+#import "QMUIAppearance.h"
static NSString * const kVideoCellIdentifier = @"video";
static NSString * const kImageOrUnknownCellIdentifier = @"imageorunknown";
@@ -40,23 +40,19 @@
@implementation QMUIImagePickerViewController (UIAppearance)
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
+}
+
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- [self appearance]; // +initialize 时就先设置好默认样式
+ [self initAppearance];
});
}
-static QMUIImagePickerViewController *imagePickerViewControllerAppearance;
-+ (nonnull instancetype)appearance {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- if (!imagePickerViewControllerAppearance) {
- imagePickerViewControllerAppearance = [[QMUIImagePickerViewController alloc] init];
- imagePickerViewControllerAppearance.minimumImageWidth = 75;
- }
- });
- return imagePickerViewControllerAppearance;
++ (void)initAppearance {
+ QMUIImagePickerViewController.appearance.minimumImageWidth = 75;
}
@end
@@ -75,96 +71,29 @@ @implementation QMUIImagePickerViewController
- (void)didInitialize {
[super didInitialize];
- if (imagePickerViewControllerAppearance) {
- // 避免 imagePickerViewControllerAppearance init 时走到这里来,导致死循环
- self.minimumImageWidth = [QMUIImagePickerViewController appearance].minimumImageWidth;
- }
+
+ [self qmui_applyAppearance];
+
_allowsMultipleSelection = YES;
_maximumSelectImageCount = INT_MAX;
_minimumSelectImageCount = 0;
_shouldShowDefaultLoadingView = YES;
- // 为了让使用者可以在 init 完就可以直接改 UI 相关的 property,这里提前触发 loadView
- [self loadViewIfNeeded];
}
- (void)dealloc {
- self.collectionView.dataSource = nil;
- self.collectionView.delegate = nil;
+ _collectionView.dataSource = nil;
+ _collectionView.delegate = nil;
}
-- (void)initSubviews {
- [super initSubviews];
-
- _collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
- CGFloat inset = PixelOne * 2; // no why, just beautiful
- self.collectionViewLayout.sectionInset = UIEdgeInsetsMake(inset, inset, inset, inset);
- self.collectionViewLayout.minimumLineSpacing = self.collectionViewLayout.sectionInset.bottom;
- self.collectionViewLayout.minimumInteritemSpacing = self.collectionViewLayout.sectionInset.left;
-
- _collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionViewLayout];
- self.collectionView.delegate = self;
- self.collectionView.dataSource = self;
- self.collectionView.showsHorizontalScrollIndicator = NO;
- self.collectionView.alwaysBounceHorizontal = NO;
- self.collectionView.backgroundColor = UIColorClear;
- [self.collectionView registerClass:[QMUIImagePickerCollectionViewCell class] forCellWithReuseIdentifier:kVideoCellIdentifier];
- [self.collectionView registerClass:[QMUIImagePickerCollectionViewCell class] forCellWithReuseIdentifier:kImageOrUnknownCellIdentifier];
- if (@available(iOS 11, *)) {
- self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- } else {
- self.automaticallyAdjustsScrollViewInsets = NO;
- }
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ self.view.backgroundColor = UIColorWhite;
[self.view addSubview:self.collectionView];
-
- // 只有允许多选时,才显示底部工具
if (self.allowsMultipleSelection) {
-
- _operationToolBarView = [[UIView alloc] init];
- self.operationToolBarView.backgroundColor = UIColorWhite;
- self.operationToolBarView.qmui_borderPosition = QMUIViewBorderPositionTop;
[self.view addSubview:self.operationToolBarView];
-
- _sendButton = [[QMUIButton alloc] init];
- self.sendButton.enabled = NO;
- self.sendButton.titleLabel.font = UIFontMake(16);
- self.sendButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
- [self.sendButton setTitleColor:UIColorMake(124, 124, 124) forState:UIControlStateNormal];
- [self.sendButton setTitleColor:UIColorGray forState:UIControlStateDisabled];
- [self.sendButton setTitle:@"发送" forState:UIControlStateNormal];
- self.sendButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -20, -12, -20);
- [self.sendButton sizeToFit];
- [self.sendButton addTarget:self action:@selector(handleSendButtonClick:) forControlEvents:UIControlEventTouchUpInside];
- [self.operationToolBarView addSubview:self.sendButton];
-
- _previewButton = [[QMUIButton alloc] init];
- self.previewButton.enabled = NO;
- self.previewButton.titleLabel.font = self.sendButton.titleLabel.font;
- [self.previewButton setTitleColor:[self.sendButton titleColorForState:UIControlStateNormal] forState:UIControlStateNormal];
- [self.previewButton setTitleColor:[self.sendButton titleColorForState:UIControlStateDisabled] forState:UIControlStateDisabled];
- [self.previewButton setTitle:@"预览" forState:UIControlStateNormal];
- self.previewButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -20, -12, -20);
- [self.previewButton sizeToFit];
- [self.previewButton addTarget:self action:@selector(handlePreviewButtonClick:) forControlEvents:UIControlEventTouchUpInside];
- [self.operationToolBarView addSubview:self.previewButton];
-
- _imageCountLabel = [[UILabel alloc] init];
- self.imageCountLabel.userInteractionEnabled = NO;// 不要影响 sendButton 的事件
- self.imageCountLabel.backgroundColor = ButtonTintColor;
- self.imageCountLabel.textColor = UIColorWhite;
- self.imageCountLabel.font = UIFontMake(12);
- self.imageCountLabel.textAlignment = NSTextAlignmentCenter;
- self.imageCountLabel.lineBreakMode = NSLineBreakByCharWrapping;
- self.imageCountLabel.layer.masksToBounds = YES;
- self.imageCountLabel.hidden = YES;
- [self.operationToolBarView addSubview:self.imageCountLabel];
}
}
-- (void)viewDidLoad {
- [super viewDidLoad];
- self.view.backgroundColor = UIColorWhite;
-}
-
- (void)setupNavigationItems {
[super setupNavigationItems];
self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithTitle:@"取消" target:self action:@selector(handleCancelPickerImage:)];
@@ -193,6 +122,16 @@ - (void)viewWillAppear:(BOOL)animated {
[self.collectionView reloadData];
}
+- (void)viewWillDisappear:(BOOL)animated {
+ [super viewWillDisappear:animated];
+
+ // 在 pop 回相簿列表时重置标志位以使下次进来 picker 时 collection 可以滚动到正确的初始位置
+ // 但不能影响从 picker 进入大图的路径
+ if (self.navigationController && ![self.navigationController.viewControllers containsObject:self]) {
+ self.hasScrollToInitialPosition = NO;
+ }
+}
+
- (void)showEmptyView {
[super showEmptyView];
self.emptyView.backgroundColor = self.view.backgroundColor; // 为了盖住背后的 collectionView,这里加个背景色(不盖住的话会看到 collectionView 先滚到列表顶部然后跳到列表底部)
@@ -217,7 +156,7 @@ - (void)viewDidLayoutSubviews {
if (!CGSizeEqualToSize(self.collectionView.frame.size, self.view.bounds.size)) {
self.collectionView.frame = self.view.bounds;
}
- UIEdgeInsets contentInset = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator, self.collectionView.qmui_safeAreaInsets.left, MAX(operationToolBarViewHeight, self.collectionView.qmui_safeAreaInsets.bottom), self.collectionView.qmui_safeAreaInsets.right);
+ UIEdgeInsets contentInset = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator, self.collectionView.safeAreaInsets.left, MAX(operationToolBarViewHeight, self.collectionView.safeAreaInsets.bottom), self.collectionView.safeAreaInsets.right);
if (!UIEdgeInsetsEqualToEdgeInsets(self.collectionView.contentInset, contentInset)) {
self.collectionView.contentInset = contentInset;
self.collectionView.scrollIndicatorInsets = UIEdgeInsetsMake(contentInset.top, 0, contentInset.bottom, 0);
@@ -249,7 +188,7 @@ - (void)refreshWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup {
if (self.shouldShowDefaultLoadingView) {
[self showEmptyViewWithLoading];
}
- dispatch_async(dispatch_get_global_queue(0, 0), ^{
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[assetsGroup enumerateAssetsWithOptions:albumSortType usingBlock:^(QMUIAsset *resultAsset) {
// 这里需要对 UI 进行操作,因此放回主线程处理
dispatch_async(dispatch_get_main_queue(), ^{
@@ -305,18 +244,118 @@ - (void)setMinimumImageWidth:(CGFloat)minimumImageWidth {
}
- (void)scrollToInitialPositionIfNeeded {
- if (self.collectionView.qmui_visible && self.isImagesAssetLoaded && !self.hasScrollToInitialPosition) {
+ if (_collectionView.qmui_visible && self.isImagesAssetLoaded && !self.hasScrollToInitialPosition) {
if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(albumSortTypeForImagePickerViewController:)] && [self.imagePickerViewControllerDelegate albumSortTypeForImagePickerViewController:self] == QMUIAlbumSortTypeReverse) {
- [self.collectionView qmui_scrollToTop];
+ [_collectionView qmui_scrollToTop];
} else {
- [self.collectionView qmui_scrollToBottom];
+ [_collectionView qmui_scrollToBottom];
}
self.hasScrollToInitialPosition = YES;
}
}
-- (void)willPopInNavigationControllerWithAnimated:(BOOL)animated {
- self.hasScrollToInitialPosition = NO;
+#pragma mark - Getters & Setters
+
+@synthesize collectionViewLayout = _collectionViewLayout;
+- (UICollectionViewFlowLayout *)collectionViewLayout {
+ if (!_collectionViewLayout) {
+ _collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
+ CGFloat inset = PixelOne * 2; // no why, just beautiful
+ _collectionViewLayout.sectionInset = UIEdgeInsetsMake(inset, inset, inset, inset);
+ _collectionViewLayout.minimumLineSpacing = _collectionViewLayout.sectionInset.bottom;
+ _collectionViewLayout.minimumInteritemSpacing = _collectionViewLayout.sectionInset.left;
+ }
+ return _collectionViewLayout;
+}
+
+@synthesize collectionView = _collectionView;
+- (UICollectionView *)collectionView {
+ if (!_collectionView) {
+ _collectionView = [[UICollectionView alloc] initWithFrame:self.isViewLoaded ? self.view.bounds : CGRectZero collectionViewLayout:self.collectionViewLayout];
+ _collectionView.delegate = self;
+ _collectionView.dataSource = self;
+ _collectionView.showsHorizontalScrollIndicator = NO;
+ _collectionView.alwaysBounceHorizontal = NO;
+ _collectionView.backgroundColor = UIColorClear;
+ [_collectionView registerClass:[QMUIImagePickerCollectionViewCell class] forCellWithReuseIdentifier:kVideoCellIdentifier];
+ [_collectionView registerClass:[QMUIImagePickerCollectionViewCell class] forCellWithReuseIdentifier:kImageOrUnknownCellIdentifier];
+ _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
+ }
+ return _collectionView;
+}
+
+@synthesize operationToolBarView = _operationToolBarView;
+- (UIView *)operationToolBarView {
+ if (!_operationToolBarView) {
+ _operationToolBarView = [[UIView alloc] init];
+ _operationToolBarView.backgroundColor = UIColorWhite;
+ _operationToolBarView.qmui_borderPosition = QMUIViewBorderPositionTop;
+
+ [_operationToolBarView addSubview:self.sendButton];
+ [_operationToolBarView addSubview:self.previewButton];
+ [_operationToolBarView addSubview:self.imageCountLabel];
+ }
+ return _operationToolBarView;
+}
+
+@synthesize sendButton = _sendButton;
+- (QMUIButton *)sendButton {
+ if (!_sendButton) {
+ _sendButton = [[QMUIButton alloc] init];
+ _sendButton.enabled = NO;
+ _sendButton.titleLabel.font = UIFontMake(16);
+ _sendButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
+ [_sendButton setTitleColor:UIColorMake(124, 124, 124) forState:UIControlStateNormal];
+ [_sendButton setTitleColor:UIColorGray forState:UIControlStateDisabled];
+ [_sendButton setTitle:@"发送" forState:UIControlStateNormal];
+ _sendButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -20, -12, -20);
+ [_sendButton sizeToFit];
+ [_sendButton addTarget:self action:@selector(handleSendButtonClick:) forControlEvents:UIControlEventTouchUpInside];
+ }
+ return _sendButton;
+}
+
+@synthesize previewButton = _previewButton;
+- (QMUIButton *)previewButton {
+ if (!_previewButton) {
+ _previewButton = [[QMUIButton alloc] init];
+ _previewButton.enabled = NO;
+ _previewButton.titleLabel.font = self.sendButton.titleLabel.font;
+ [_previewButton setTitleColor:[self.sendButton titleColorForState:UIControlStateNormal] forState:UIControlStateNormal];
+ [_previewButton setTitleColor:[self.sendButton titleColorForState:UIControlStateDisabled] forState:UIControlStateDisabled];
+ [_previewButton setTitle:@"预览" forState:UIControlStateNormal];
+ _previewButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -20, -12, -20);
+ [_previewButton sizeToFit];
+ [_previewButton addTarget:self action:@selector(handlePreviewButtonClick:) forControlEvents:UIControlEventTouchUpInside];
+ }
+ return _previewButton;
+}
+
+@synthesize imageCountLabel = _imageCountLabel;
+- (UILabel *)imageCountLabel {
+ if (!_imageCountLabel) {
+ _imageCountLabel = [[UILabel alloc] init];
+ _imageCountLabel.userInteractionEnabled = NO;// 不要影响 sendButton 的事件
+ _imageCountLabel.backgroundColor = ButtonTintColor;
+ _imageCountLabel.textColor = UIColorWhite;
+ _imageCountLabel.font = UIFontMake(12);
+ _imageCountLabel.textAlignment = NSTextAlignmentCenter;
+ _imageCountLabel.lineBreakMode = NSLineBreakByCharWrapping;
+ _imageCountLabel.layer.masksToBounds = YES;
+ _imageCountLabel.hidden = YES;
+ }
+ return _imageCountLabel;
+}
+
+- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection {
+ _allowsMultipleSelection = allowsMultipleSelection;
+ if (self.isViewLoaded) {
+ if (_allowsMultipleSelection) {
+ [self.view addSubview:self.operationToolBarView];
+ } else {
+ [_operationToolBarView removeFromSuperview];
+ }
+ }
}
#pragma mark -
@@ -408,13 +447,13 @@ - (void)handleCancelPickerImage:(id)sender {
}
- (void)handleCheckBoxButtonClick:(UIButton *)checkboxButton {
- NSIndexPath *indexPath = [self.collectionView qmui_indexPathForItemAtView:checkboxButton];
+ NSIndexPath *indexPath = [_collectionView qmui_indexPathForItemAtView:checkboxButton];
if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:shouldCheckImageAtIndex:)] && ![self.imagePickerViewControllerDelegate imagePickerViewController:self shouldCheckImageAtIndex:indexPath.item]) {
return;
}
- QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
+ QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[_collectionView cellForItemAtIndexPath:indexPath];
QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item];
if (cell.checked) {
// 移除选中状态
@@ -486,7 +525,7 @@ - (void)updateImageCountAndCheckLimited {
- (void)requestImageWithIndexPath:(NSIndexPath *)indexPath {
// 发出请求获取大图,如果图片在 iCloud,则会发出网络请求下载图片。这里同时保存请求 id,供取消请求使用
QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item];
- QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
+ QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[_collectionView cellForItemAtIndexPath:indexPath];
imageAsset.requestID = [imageAsset requestOriginImageWithCompletion:^(UIImage *result, NSDictionary *info) {
BOOL downloadSucceed = (result && !info) || (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue]);
diff --git a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h
index 64362020..a0b3b796 100644
--- a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h
+++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h
@@ -1,13 +1,13 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
-// QMUINavigationBar+Transition.h
+// UINavigationBar+Transition.h
// qmui
//
// Created by QMUI Team on 11/25/16.
@@ -18,6 +18,18 @@
@interface UINavigationBar (Transition)
/// 用来模仿真的navBar,配合 UINavigationController+NavigationBarTransition 在转场过程中存在的一条假navBar
-@property(nonatomic, strong) UINavigationBar *transitionNavigationBar;
+@property(nonatomic, weak) UINavigationBar *qmuinb_copyStylesToBar;
+@end
+
+@interface _QMUITransitionNavigationBar : UINavigationBar
+
+@property(nonatomic, weak) UIViewController *parentViewController;
+
+// 建立假 bar 到真 bar 的关系,内部会通过 qmuinb_copyStylesToBar 同时设置真 bar 到假 bar 的关系
+@property(nonatomic, weak) UINavigationBar *originalNavigationBar;
+
+@property(nonatomic, assign) BOOL shouldPreventAppearance;
+// 根据当前的系统导航栏布局,刷新自身在 vc.view 上的布局
+- (void)updateLayout;
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m
index 93863698..3e1c5ea7 100644
--- a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m
+++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m
@@ -1,13 +1,13 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
-// QMUINavigationBar+Transition.m
+// UINavigationBar+Transition.m
// qmui
//
// Created by QMUI Team on 11/25/16.
@@ -15,32 +15,267 @@
#import "UINavigationBar+Transition.h"
#import "QMUICore.h"
+#import "UINavigationBar+QMUI.h"
+#import "UINavigationBar+QMUIBarProtocol.h"
+#import "QMUIWeakObjectContainer.h"
+#import "UIImage+QMUI.h"
@implementation UINavigationBar (Transition)
-QMUISynthesizeIdStrongProperty(transitionNavigationBar, setTransitionNavigationBar)
-
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setShadowImage:), UIImage *, ^(UINavigationBar *selfObject, UIImage *firstArgv) {
- if (selfObject.transitionNavigationBar) {
- selfObject.transitionNavigationBar.shadowImage = firstArgv;
- }
+
+ if (@available(iOS 15.0, *)) {
+
+ OverrideImplementation([UINavigationBar class], @selector(setStandardAppearance:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UINavigationBarAppearance *);
+ originSelectorIMP = (void (*)(id, SEL, UINavigationBarAppearance *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, appearance);
+
+ if (selfObject.qmuinb_copyStylesToBar) {
+ selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance;
+ }
+ };
+ });
+
+ OverrideImplementation([UINavigationBar class], @selector(setScrollEdgeAppearance:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UINavigationBarAppearance *);
+ originSelectorIMP = (void (*)(id, SEL, UINavigationBarAppearance *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, appearance);
+
+ if (selfObject.qmuinb_copyStylesToBar) {
+ selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance;
+ }
+ };
+ });
+ }
+
+ OverrideImplementation([UINavigationBar class], @selector(setBarStyle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject, UIBarStyle barStyle) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIBarStyle);
+ originSelectorIMP = (void (*)(id, SEL, UIBarStyle))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, barStyle);
+
+ if (selfObject.qmuinb_copyStylesToBar && selfObject.qmuinb_copyStylesToBar.barStyle != barStyle) {
+ selfObject.qmuinb_copyStylesToBar.barStyle = barStyle;
+ }
+ };
+ });
+
+ OverrideImplementation([UINavigationBar class], @selector(setBarTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject, UIColor *barTintColor) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIColor *);
+ originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, barTintColor);
+
+ if (selfObject.qmuinb_copyStylesToBar && ![selfObject.qmuinb_copyStylesToBar.barTintColor isEqual:barTintColor]) {
+ selfObject.qmuinb_copyStylesToBar.barTintColor = barTintColor;
+ }
+ };
});
- ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setBarTintColor:), UIColor *, ^(UINavigationBar *selfObject, UIColor *firstArgv) {
- if (selfObject.transitionNavigationBar) {
- selfObject.transitionNavigationBar.barTintColor = firstArgv;
- }
+ OverrideImplementation([UINavigationBar class], @selector(setBackgroundImage:forBarMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject, UIImage *image, UIBarMetrics barMetrics) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIImage *, UIBarMetrics);
+ originSelectorIMP = (void (*)(id, SEL, UIImage *, UIBarMetrics))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, image, barMetrics);
+
+ if (selfObject.qmuinb_copyStylesToBar) {
+ [selfObject.qmuinb_copyStylesToBar setBackgroundImage:image forBarMetrics:barMetrics];
+ }
+ };
});
- ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationBar class], @selector(setBackgroundImage:forBarMetrics:), UIImage *, UIBarMetrics, ^(UINavigationBar *selfObject, UIImage *backgroundImage, UIBarMetrics barMetrics) {
- if (selfObject.transitionNavigationBar) {
- [selfObject.transitionNavigationBar setBackgroundImage:backgroundImage forBarMetrics:barMetrics];
- }
+ OverrideImplementation([UINavigationBar class], @selector(setShadowImage:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject, UIImage *shadowImage) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIImage *);
+ originSelectorIMP = (void (*)(id, SEL, UIImage *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, shadowImage);
+
+ if (selfObject.qmuinb_copyStylesToBar) {
+ selfObject.qmuinb_copyStylesToBar.shadowImage = shadowImage;
+ }
+ };
+ });
+
+ OverrideImplementation([UINavigationBar class], @selector(setQmui_effect:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject, UIBlurEffect *firstArgv) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIBlurEffect *);
+ originSelectorIMP = (void (*)(id, SEL, UIBlurEffect *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, firstArgv);
+
+ if (selfObject.qmuinb_copyStylesToBar) {
+ selfObject.qmuinb_copyStylesToBar.qmui_effect = firstArgv;
+ }
+ };
+ });
+
+ OverrideImplementation([UINavigationBar class], @selector(setQmui_effectForegroundColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject, UIColor *firstArgv) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIColor *);
+ originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, firstArgv);
+
+ if (selfObject.qmuinb_copyStylesToBar) {
+ selfObject.qmuinb_copyStylesToBar.qmui_effectForegroundColor = firstArgv;
+ }
+ };
});
});
}
+static char kAssociatedObjectKey_copyStylesToBar;
+- (void)setQmuinb_copyStylesToBar:(UINavigationBar *)copyStylesToBar {
+ QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_copyStylesToBar);
+ if (!weakContainer) {
+ weakContainer = [[QMUIWeakObjectContainer alloc] init];
+ }
+ weakContainer.object = copyStylesToBar;
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_copyStylesToBar, weakContainer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+
+ if (!copyStylesToBar) return;
+
+#ifdef IOS15_SDK_ALLOWED
+ if (@available(iOS 15.0, *)) {
+ copyStylesToBar.standardAppearance = self.standardAppearance;
+ copyStylesToBar.scrollEdgeAppearance = self.scrollEdgeAppearance;
+ } else {
+#endif
+ UIImage *backgroundImage = [self backgroundImageForBarMetrics:UIBarMetricsDefault];
+ if (backgroundImage && backgroundImage.size.width <= 0 && backgroundImage.size.height <= 0) {
+ // 假设这里的图片时通过`[UIImage new]`这种形式创建的,那么会navBar会奇怪地显示为系统默认navBar的样式。不知道为什么 navController 设置自己的 navBar 为 [UIImage new] 却没事,所以这里做个保护。
+ backgroundImage = [UIImage qmui_imageWithColor:UIColorClear];
+ }
+ [copyStylesToBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault];
+
+ copyStylesToBar.shadowImage = self.shadowImage;
+
+ if (copyStylesToBar.barStyle != self.barStyle) {
+ copyStylesToBar.barStyle = self.barStyle;
+ }
+
+ // setTranslucent 要在 setBackgroundImage 之后,因为 setBackgroundImage 内部会改变 translucent 的值
+ if (copyStylesToBar.translucent != self.translucent) {
+ copyStylesToBar.translucent = self.translucent;
+ }
+
+ if (![copyStylesToBar.barTintColor isEqual:self.barTintColor]) {
+ copyStylesToBar.barTintColor = self.barTintColor;
+ }
+
+#ifdef IOS15_SDK_ALLOWED
+ }
+#endif
+
+ copyStylesToBar.qmui_effect = self.qmui_effect;
+ copyStylesToBar.qmui_effectForegroundColor = self.qmui_effectForegroundColor;
+}
+
+- (UINavigationBar *)qmuinb_copyStylesToBar {
+ return (UINavigationBar *)((QMUIWeakObjectContainer *)objc_getAssociatedObject(self, &kAssociatedObjectKey_copyStylesToBar)).object;
+}
+
+@end
+
+@implementation _QMUITransitionNavigationBar
+
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ // iOS 14 开启 customNavigationBarTransitionKey 的情况下转场效果错误
+ // https://github.com/Tencent/QMUI_iOS/issues/1081
+ if (@available(iOS 14.0, *)) {
+ // - [UINavigationBar _accessibility_navigationController]
+ OverrideImplementation([_QMUITransitionNavigationBar class], NSSelectorFromString([NSString stringWithFormat:@"_%@_%@", @"accessibility", @"navigationController"]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^UINavigationController *(_QMUITransitionNavigationBar *selfObject) {
+ if (selfObject.originalNavigationBar) {
+ BeginIgnorePerformSelectorLeaksWarning
+ return [selfObject.originalNavigationBar performSelector:originCMD];
+ EndIgnorePerformSelectorLeaksWarning
+ }
+
+ // call super
+ UINavigationController *(*originSelectorIMP)(id, SEL);
+ originSelectorIMP = (UINavigationController *(*)(id, SEL))originalIMPProvider();
+ UINavigationController *result = originSelectorIMP(selfObject, originCMD);
+ return result;
+ };
+ });
+ }
+
+#ifdef IOS15_SDK_ALLOWED
+ if (@available(iOS 15.0, *)) {
+ // - [UINavigationBar _didMoveFromWindow:toWindow:]
+ OverrideImplementation([_QMUITransitionNavigationBar class], NSSelectorFromString(@"_didMoveFromWindow:toWindow:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(_QMUITransitionNavigationBar *selfObject, UIWindow *firstArgv, UIWindow *secondArgv) {
+
+ if (selfObject.shouldPreventAppearance) {
+ return;
+ }
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIWindow *, UIWindow *);
+ originSelectorIMP = (void (*)(id, SEL, UIWindow *, UIWindow *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv);
+ };
+ });
+ }
+#endif
+
+ });
+}
+
+- (void)setOriginalNavigationBar:(UINavigationBar *)originBar {
+ _originalNavigationBar = originBar;
+
+ // 只复制当前 originBar 的样式,所以复制完立马就清空
+ originBar.qmuinb_copyStylesToBar = self;
+ originBar.qmuinb_copyStylesToBar = nil;
+
+ [self updateLayout];
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ // 实测 iOS 11 Beta 1-5 里,自己 init 的 navigationBar.backgroundView.height 默认一直是 44,所以才加上这个兼容
+ self.qmui_backgroundView.frame = self.bounds;
+}
+
+// NavBarRemoveBackgroundEffectAutomatically 在开启了 AutomaticCustomNavigationBarTransitionStyle 时可能对假 bar 无效
+// https://github.com/Tencent/QMUI_iOS/issues/1330
+- (void)didAddSubview:(UIView *)subview {
+ [super didAddSubview:subview];
+ if (subview == self.qmui_backgroundView) {
+ [subview qmui_performSelector:NSSelectorFromString(@"updateBackground") withArguments:nil];
+ }
+}
+
+- (void)updateLayout {
+ if ([self.parentViewController isViewLoaded] && self.originalNavigationBar) {
+ [self.parentViewController.view bringSubviewToFront:self];
+ UIView *backgroundView = self.originalNavigationBar.qmui_backgroundView;
+ CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.parentViewController.view];
+ self.frame = CGRectSetX(rect, 0);// push/pop 过程中系统的导航栏转换过来的 x 可能是 112、-112
+ }
+}
+
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h
index fa807c4f..c42103e8 100644
--- a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h
+++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// UINavigationController+NavigationBarTransition.h
diff --git a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m
index 5500abda..21982331 100644
--- a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m
+++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// UINavigationController+NavigationBarTransition.m
@@ -20,34 +20,20 @@
#import "UIImage+QMUI.h"
#import "UIViewController+QMUI.h"
#import "UINavigationBar+Transition.h"
-#import "QMUICommonViewController.h"
#import "QMUINavigationTitleView.h"
#import "UINavigationBar+QMUI.h"
+#import "UINavigationBar+QMUIBarProtocol.h"
#import "UIView+QMUI.h"
#import "QMUILog.h"
-@interface _QMUITransitionNavigationBar : UINavigationBar
-
-@end
-
-@implementation _QMUITransitionNavigationBar
-
-- (void)layoutSubviews {
- [super layoutSubviews];
- if (@available(iOS 11, *)) {
- // iOS 11 以前,自己 init 的 navigationBar,它的 backgroundView 默认会一直保持与 navigationBar 的高度相等,但 iOS 11 Beta 1-5 里,自己 init 的 navigationBar.backgroundView.height 默认一直是 44,所以才加上这个兼容
- self.qmui_backgroundView.frame = self.bounds;
- }
-}
-
-@end
-
/**
* 为了响应NavigationBarTransition分类的功能,UIViewController需要做一些相应的支持。
* @see UINavigationController+NavigationBarTransition.h
*/
@interface UIViewController (NavigationBarTransition)
+@property(nonatomic, assign) BOOL qmuinb_shouldShowTransitionBar;
+
/// 用来模仿真的navBar的,在转场过程中存在的一条假navBar
@property(nonatomic, strong) _QMUITransitionNavigationBar *transitionNavigationBar;
@@ -57,11 +43,52 @@ @interface UIViewController (NavigationBarTransition)
/// 原始containerView的背景色
@property(nonatomic, strong) UIColor *originContainerViewBackgroundColor;
-/// 添加假的navBar
-- (void)addTransitionNavigationBarIfNeeded;
+@end
+
+@interface UILabel (NavigationBarTransition)
+@property(nonatomic, strong) UIColor *qmui_specifiedTextColor;
+@end
+
+@implementation UILabel (NavigationBarTransition)
-/// .m文件里自己赋值和使用。因为有些特殊情况下viewDidAppear之后,有可能还会调用到viewWillLayoutSubviews,导致原始的navBar隐藏,所以用这个属性做个保护。
-@property(nonatomic, assign) BOOL lockTransitionNavigationBar;
+QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextColor)
+
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ OverrideImplementation(NSClassFromString(@"UIButtonLabel"), @selector(setAttributedText:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UILabel *selfObject, NSAttributedString *attributedText) {
+
+ if (selfObject.qmui_specifiedTextColor) {
+ NSMutableAttributedString *mutableAttributedText = [attributedText isKindOfClass:NSMutableAttributedString.class] ? attributedText : [attributedText mutableCopy];
+ [mutableAttributedText addAttributes:@{ NSForegroundColorAttributeName : selfObject.qmui_specifiedTextColor} range:NSMakeRange(0, mutableAttributedText.length)];
+ attributedText = mutableAttributedText;
+ }
+
+ void (*originSelectorIMP)(id, SEL, NSAttributedString *);
+ originSelectorIMP = (void (*)(id, SEL, NSAttributedString *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, attributedText);
+ };
+ });
+ });
+}
+
+@end
+
+@implementation UINavigationBar (NavigationBarTransition)
+
+/// 获取系统自带的返回按钮 Label,如果在转场时,会获取到最上面控制器的。
+- (UILabel *)qmui_backButtonLabel {
+ __block UILabel *backButtonLabel = nil;
+ [self.qmui_contentView.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
+ if ([subview isKindOfClass:NSClassFromString(@"_UIButtonBarButton")]) {
+ UIButton *titleButton = [subview valueForKeyPath:@"visualProvider.titleButton"];
+ backButtonLabel = titleButton.titleLabel;
+ *stop = YES;
+ }
+ }];
+ return backButtonLabel;
+}
@end
@@ -74,62 +101,68 @@ + (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- OverrideImplementation([UIViewController class], @selector(viewWillAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^(UIViewController *selfObject, BOOL firstArgv) {
+ ExtendImplementationOfVoidMethodWithoutArguments([UINavigationController class], @selector(qmui_didInitialize), ^(UINavigationController *selfObject) {
+ [selfObject qmui_addNavigationActionDidChangeBlock:^(QMUINavigationAction action, BOOL animated, __kindof UINavigationController * _Nullable weakNavigationController, __kindof UIViewController * _Nullable appearingViewController, NSArray<__kindof UIViewController *> * _Nullable disappearingViewControllers) {
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- // 放在最前面,留一个时机给业务可以覆盖
- [selfObject renderNavigationStyleInViewController:selfObject animated:firstArgv];
+ // 左右两个界面都必须存在
+ UIViewController *disappearingViewController = disappearingViewControllers.lastObject;
+ if (!appearingViewController || !disappearingViewController) {
+ return;
}
- // call super
- void (*originSelectorIMP)(id, SEL, BOOL);
- originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider();
- originSelectorIMP(selfObject, originCMD, firstArgv);
- };
- });
-
- OverrideImplementation([UIViewController class], @selector(viewDidAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^(UIViewController *selfObject, BOOL firstArgv) {
-
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- selfObject.lockTransitionNavigationBar = YES;
-
- if (selfObject.transitionNavigationBar) {
-
- [UIViewController replaceStyleForNavigationBar:selfObject.transitionNavigationBar withNavigationBar:selfObject.navigationController.navigationBar];
- [selfObject removeTransitionNavigationBar];
-
- id transitionCoordinator = selfObject.transitionCoordinator;
- [transitionCoordinator containerView].backgroundColor = selfObject.originContainerViewBackgroundColor;
+ switch (action) {
+ case QMUINavigationActionDidPush:
+ case QMUINavigationActionWillPop:
+ case QMUINavigationActionDidSet: {
+ BOOL shouldCustomNavigationBarTransition =
+ [weakNavigationController shouldCustomTransitionAutomaticallyForOperation:UINavigationControllerOperationPush firstViewController:disappearingViewController secondViewController:appearingViewController];
+ if (shouldCustomNavigationBarTransition) {
+ disappearingViewController.qmuinb_shouldShowTransitionBar = YES;
+ appearingViewController.qmuinb_shouldShowTransitionBar = YES;
+
+ // 只绑定即将显示的 vc 的 bar,注意可能在 setNavigationBarHidden: 里被覆盖,引起下述问题:
+ // https://github.com/Tencent/QMUI_iOS/issues/1335
+ weakNavigationController.navigationBar.qmuinb_copyStylesToBar = appearingViewController.transitionNavigationBar;
+ }
}
-
- if ([selfObject.navigationController.viewControllers containsObject:selfObject]) {
- // 防止一些 childViewController 走到这里
- selfObject.prefersNavigationBarBackgroundViewHidden = NO;
+ break;
+ case QMUINavigationActionPushCompleted:
+ case QMUINavigationActionPopCompleted:
+ case QMUINavigationActionSetCompleted: {
+ disappearingViewController.qmuinb_shouldShowTransitionBar = NO;
+ appearingViewController.qmuinb_shouldShowTransitionBar = NO;
+ weakNavigationController.navigationBar.qmuinb_copyStylesToBar = nil;
}
+ break;
+ default:
+ break;
}
+ }];
+ });
+
+ OverrideImplementation([UINavigationController class], @selector(setNavigationBarHidden:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationController *selfObject, BOOL hidden, BOOL animated) {
// call super
- void (*originSelectorIMP)(id, SEL, BOOL);
- originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider();
- originSelectorIMP(selfObject, originCMD, firstArgv);
+ void (*originSelectorIMP)(id, SEL, BOOL, BOOL);
+ originSelectorIMP = (void (*)(id, SEL, BOOL, BOOL))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, hidden, animated);
+
+ if ((selfObject.qmui_isPushing || selfObject.qmui_isPopping) && selfObject.topViewController.qmuinb_shouldShowTransitionBar) {
+ if (hidden) {
+ [selfObject.topViewController removeTransitionNavigationBar];
+ } else {
+ [selfObject.topViewController addTransitionNavigationBarAndBindNavigationBar:YES];
+ }
+ }
};
});
- OverrideImplementation([UIViewController class], @selector(viewDidDisappear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ OverrideImplementation([UIViewController class], @selector(viewWillAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UIViewController *selfObject, BOOL firstArgv) {
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- selfObject.lockTransitionNavigationBar = NO;
-
- if (selfObject.transitionNavigationBar) {
- [selfObject removeTransitionNavigationBar];
- }
- }
+ // 放在最前面,留一个时机给业务可以覆盖
+ [selfObject renderNavigationBarStyleAnimated:firstArgv];
// call super
void (*originSelectorIMP)(id, SEL, BOOL);
@@ -140,42 +173,7 @@ + (void)load {
OverrideImplementation([UIViewController class], @selector(viewWillLayoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UIViewController *selfObject) {
-
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- if (![selfObject.navigationController.delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) {
-
- id transitionCoordinator = selfObject.transitionCoordinator;
- UIViewController *fromViewController = [transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey];
- UIViewController *toViewController = [transitionCoordinator viewControllerForKey:UITransitionContextToViewControllerKey];
-
- BOOL isCurrentToViewController = (selfObject == selfObject.navigationController.viewControllers.lastObject && selfObject == toViewController);
-
- if (isCurrentToViewController && !selfObject.lockTransitionNavigationBar) {
-
- BOOL shouldCustomNavigationBarTransition = NO;
-
- if (!selfObject.transitionNavigationBar) {
-
- if ([selfObject shouldCustomTransitionAutomaticallyWithFirstViewController:fromViewController secondViewController:toViewController]) {
- shouldCustomNavigationBarTransition = YES;
- }
-
- if (shouldCustomNavigationBarTransition) {
- if (selfObject.navigationController.navigationBar.translucent) {
- // 如果原生bar是半透明的,需要给containerView加个背景色,否则有可能会看到下面的默认黑色背景色
- toViewController.originContainerViewBackgroundColor = [transitionCoordinator containerView].backgroundColor;
- [transitionCoordinator containerView].backgroundColor = [selfObject containerViewBackgroundColor];
- }
- [selfObject addTransitionNavigationBarIfNeeded];
- [selfObject resizeTransitionNavigationBarFrame];
- selfObject.navigationController.navigationBar.transitionNavigationBar = selfObject.transitionNavigationBar;
- selfObject.prefersNavigationBarBackgroundViewHidden = YES;
- }
- }
- }
- }
- }
+ [selfObject.transitionNavigationBar updateLayout];
// call super
void (*originSelectorIMP)(id, SEL);
@@ -183,174 +181,249 @@ + (void)load {
originSelectorIMP(selfObject, originCMD);
};
});
+
+ // 修复 UISearchController push 到导航栏隐藏的界面时,会强制把导航栏重新显示出来的 bug
+ // https://github.com/Tencent/QMUI_iOS/issues/479
+ // _navigationControllerWillShowViewController:
+ SEL selector = NSSelectorFromString([NSString stringWithFormat:@"_%@%@:", @"navigationController", @"WillShowViewController"]);
+ QMUIAssert([[UISearchController class] instancesRespondToSelector:selector], @"UIViewController (NavigationBarTransition)", @"iOS 版本更新导致 UISearchController 无法响应方法 %@", NSStringFromSelector(selector));
+ OverrideImplementation([UISearchController class], selector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UISearchController *selfObject, NSNotification *firstArgv) {
+ UIViewController *nextViewController = firstArgv.userInfo[@"UINavigationControllerNextVisibleViewController"];
+ if (![nextViewController canCustomNavigationBarTransitionIfBarHiddenable]) {
+ void (*originSelectorIMP)(id, SEL, NSNotification *);
+ originSelectorIMP = (void (*)(id, SEL, NSNotification *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, firstArgv);
+ }
+ };
+ });
+
+ if (@available(iOS 15.0, *)) {
+ // - [UINavigationBar didMoveToWindow]
+ OverrideImplementation([UINavigationBar class], @selector(didMoveToWindow), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL);
+ originSelectorIMP = (void (*)(id, SEL))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD);
+
+ // 由于 renderNavigationBarStyleAnimated: 里对导航栏尚未添加到 window 上(UIAppearance 尚未被应用)的情况,跳过了 renderNavigationBarAppearanceAnimated:,所以这里在导航栏添加到 window 上时刷新一下导航栏样式
+ // https://github.com/Tencent/QMUI_iOS/issues/1437
+ if (selfObject.window) {
+ UINavigationController *nav = (UINavigationController *)selfObject.qmui_viewController;
+ if (![nav isKindOfClass:UINavigationController.class]) return;
+ UIViewController *topViewController = nav.topViewController;
+ if (topViewController.qmui_visibleState & QMUIViewControllerVisible) {// 加上这个 visibleState 的判断是因为一个普通的 UINavigationController 被初始化后导航栏默认就有一个 didMoveToWindow 的时机,这个时机里 topViewController 尚未触发 viewWillAppear:,如果不判断 visibleState,就会导致在过早的时候去设置导航栏样式,然后 viewWillAppear: 时又设置了一次。
+ [topViewController renderNavigationBarAppearanceAnimated:NO];
+ }
+ }
+ };
+ });
+ }
});
}
-- (void)addTransitionNavigationBarIfNeeded {
-
- if (!self.view.qmui_visible || !self.navigationController.navigationBar) {
+- (void)addTransitionNavigationBarAndBindNavigationBar:(BOOL)shouldBind {
+ // add 时虽然过滤了 navigationBarHidden 的条件,但可能在 push/pop 时,新界面暂时还没刷新导航栏的显隐状态,所以还是需要在 viewWillLayoutSubviews 那边再重新根据 navigationBarHidden 的值来决定是否隐藏假 bar
+ if (!self.qmuinb_shouldShowTransitionBar || self.transitionNavigationBar || !self.navigationController.navigationBar || self.navigationController.navigationBarHidden) {
return;
}
- UINavigationBar *originBar = self.navigationController.navigationBar;
_QMUITransitionNavigationBar *customBar = [[_QMUITransitionNavigationBar alloc] init];
+ customBar.parentViewController = self;
+ self.transitionNavigationBar = customBar;
- if (customBar.barStyle != originBar.barStyle) {
- customBar.barStyle = originBar.barStyle;
+ // iOS 15 里,假 bar 在 add 到界面上时会被强制同步为 UIAppearance 的值,不管你之前是否设置过自己的样式。而且在那个 runloop 内不管你后续怎么更新 standardAppearance,都会呈现出 UIAppearance 里的统一的值的样式。所以这里一方面屏蔽 didMoveToWindow,从而避免在这时候应用 UIAppearance,另一方面要保证先 add 到界面上再同步当前导航栏的样式。
+ // 经测试只有 push 或 push 动画的 set 需要这么处理,pop 及 pop 动画的 set 没问题
+ // iOS 14 及以下没这种问题。
+ // https://github.com/Tencent/QMUI_iOS/issues/1501
+ if (@available(iOS 15.0, *)) {
+ BOOL isPush = self.navigationController.qmui_navigationAction == QMUINavigationActionDidPush;
+ BOOL isSet = self.navigationController.qmui_navigationAction == QMUINavigationActionDidSet;
+ BOOL isPopAnimation = isSet && self.navigationController.qmui_lastOperation == UINavigationControllerOperationPop;
+ if (isPush || (isSet && !isPopAnimation)) {
+ customBar.shouldPreventAppearance = YES;
+ }
}
-
- if (customBar.translucent != originBar.translucent) {
- customBar.translucent = originBar.translucent;
+ [self.view addSubview:customBar];
+ customBar.originalNavigationBar = self.navigationController.navigationBar;// 注意这里内部不会保留真 bar 和假 bar 的 copy 关系
+ if (shouldBind) {
+ self.navigationController.navigationBar.qmuinb_copyStylesToBar = customBar;
}
-
- if (![customBar.barTintColor isEqual:originBar.barTintColor]) {
- customBar.barTintColor = originBar.barTintColor;
+}
+
+- (void)removeTransitionNavigationBar {
+ if (self.transitionNavigationBar) {
+ [self.transitionNavigationBar removeFromSuperview];
+ self.transitionNavigationBar = nil;
+ id transitionCoordinator = self.transitionCoordinator;
+ if (self.navigationController.navigationBar.translucent && self.originContainerViewBackgroundColor) {
+ [transitionCoordinator containerView].backgroundColor = self.originContainerViewBackgroundColor;
+ }
}
+}
+
+#pragma mark - 工具方法
+
+// 根据当前的viewController,统一处理导航栏的显隐、样式
+- (void)renderNavigationBarStyleAnimated:(BOOL)animated {
- UIImage *backgroundImage = [originBar backgroundImageForBarMetrics:UIBarMetricsDefault];
- if (backgroundImage && backgroundImage.size.width <= 0 && backgroundImage.size.height <= 0) {
- // 假设这里的图片时通过`[UIImage new]`这种形式创建的,那么会navBar会奇怪地显示为系统默认navBar的样式。不知道为什么 navController 设置自己的 navBar 为 [UIImage new] 却没事,所以这里做个保护。
- backgroundImage = [UIImage qmui_imageWithColor:UIColorClear];
+ // 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController
+ if (![self.navigationController.viewControllers containsObject:self]) {
+ return;
}
- [customBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault];
- [customBar setShadowImage:originBar.shadowImage];
+ if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) {
+ return;
+ }
- self.transitionNavigationBar = customBar;
- [self resizeTransitionNavigationBarFrame];
+ // 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式
+ UIViewController *vc = (UIViewController *)self;
+ UINavigationController *navigationController = vc.navigationController;
- if (!self.navigationController.navigationBarHidden) {
- [self.view addSubview:self.transitionNavigationBar];
+ // 显示/隐藏 导航栏
+ if ([vc canCustomNavigationBarTransitionIfBarHiddenable]) {
+ if ([vc hideNavigationBarWhenTransitioning]) {
+ if (!navigationController.isNavigationBarHidden) {
+ [navigationController setNavigationBarHidden:YES animated:animated];
+ }
+ } else {
+ if (navigationController.isNavigationBarHidden) {
+ [navigationController setNavigationBarHidden:NO animated:animated];
+ }
+ }
}
- CGRect viewRect = [self.navigationController.view convertRect:self.view.frame fromView:self.view.superview];
- if (viewRect.origin.y != 0 && self.view.clipsToBounds) {
- QMUILog(@"UINavigationController+NavigationBarTransition", @"⚠️⚠️⚠️注意啦:当前界面 controller.view = %@ 布局并没有从屏幕顶部开始,可能会导致自定义导航栏转场的假 bar 看不到", self);
+ // 仅当导航栏被添加到 window 之后(UIAppearance 被应用之后),业务才可以设置导航栏的样式,否则在 UINavigationBar (QMUI) 里获取到的 navigationBar.standardAppearance 是系统默认的样式而不是 App 全局配置的样式,导致后续导航栏样式都是错的。
+ // https://github.com/Tencent/QMUI_iOS/issues/1437
+ if (@available(iOS 15.0, *)) {
+ if (!navigationController.navigationBar.window) {
+ return;
+ }
}
+
+ [self renderNavigationBarAppearanceAnimated:animated];
}
-- (void)removeTransitionNavigationBar {
- if (!self.transitionNavigationBar) {
+// 仅处理导航栏的样式,不涉及显隐
+- (void)renderNavigationBarAppearanceAnimated:(BOOL)animated {
+
+ // 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController
+ if (![self.navigationController.viewControllers containsObject:self]) {
return;
}
- [self.transitionNavigationBar removeFromSuperview];
- self.transitionNavigationBar = nil;
-}
-
-- (void)resizeTransitionNavigationBarFrame {
- if (!self.view.qmui_visible) {
+
+ if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) {
return;
}
- UIView *backgroundView = self.navigationController.navigationBar.qmui_backgroundView;
- CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view];
- self.transitionNavigationBar.frame = rect;
+
+ // 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式
+ UIViewController *vc = (UIViewController *)self;
+ UINavigationController *navigationController = vc.navigationController;
+
+ // 导航栏的背景色
+ if ([vc respondsToSelector:@selector(qmui_navigationBarBarTintColor)]) {
+ UIColor *barTintColor = [vc qmui_navigationBarBarTintColor];
+ navigationController.navigationBar.barTintColor = barTintColor;
+ } else if (QMUICMIActivated) {
+ navigationController.navigationBar.barTintColor = UINavigationBar.qmui_appearanceConfigured.barTintColor;
+ }
+
+ // 导航栏的背景
+ if ([vc respondsToSelector:@selector(qmui_navigationBarBackgroundImage)]) {
+ UIImage *backgroundImage = [vc qmui_navigationBarBackgroundImage];
+ [navigationController.navigationBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault];
+ } else if (QMUICMIActivated) {
+ [navigationController.navigationBar setBackgroundImage:[UINavigationBar.qmui_appearanceConfigured backgroundImageForBarMetrics:UIBarMetricsDefault] forBarMetrics:UIBarMetricsDefault];
+ }
+
+ // 导航栏的 style
+ if ([vc respondsToSelector:@selector(qmui_navigationBarStyle)]) {
+ UIBarStyle barStyle = [vc qmui_navigationBarStyle];
+ navigationController.navigationBar.barStyle = barStyle;
+ } else if (QMUICMIActivated) {
+ navigationController.navigationBar.barStyle = UINavigationBar.qmui_appearanceConfigured.barStyle;
+ }
+
+ // 导航栏底部的分隔线
+ if ([vc respondsToSelector:@selector(qmui_navigationBarShadowImage)]) {
+ navigationController.navigationBar.shadowImage = [vc qmui_navigationBarShadowImage];
+ } else if (QMUICMIActivated) {
+ navigationController.navigationBar.shadowImage = NavBarShadowImage;
+ }
+
+ // 导航栏上控件的主题色
+ UIColor *tintColor =
+ [vc respondsToSelector:@selector(qmui_navigationBarTintColor)] ? [vc qmui_navigationBarTintColor] :
+ QMUICMIActivated ? NavBarTintColor : nil;
+ if (tintColor) {
+ // https://github.com/Tencent/QMUI_iOS/issues/654
+ // 改变 navigationBar.tintColor 后会同步改变返回按钮的文字颜色,在 iOS 10及以下,把修改 tintColor 的代码包裹在 animateAlongsideTransition 中能实现转场过渡,而从 iOS 11 开始不生效,现象是:修改了 navigationBar.tintColor 后,返回按钮的文字颜色瞬间变化。
+ // 为了实现转场过渡,不要让返回按钮的文字瞬间变化,在转场前锁定 topViewController 所属的 backButtonLabel 颜色,这样在转场过程中改变了 navBar 的 tintColor 不会影响到他。
+ if (navigationController.qmui_isPopping) {
+ UILabel *backButtonLabel = navigationController.navigationBar.qmui_backButtonLabel;
+ if (backButtonLabel) {
+ backButtonLabel.qmui_specifiedTextColor = backButtonLabel.textColor;
+ [vc qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) {
+ backButtonLabel.qmui_specifiedTextColor = nil;
+ }];
+ }
+ }
+
+ [vc qmui_animateAlongsideTransition:^(id _Nonnull context) {
+ navigationController.navigationBar.tintColor = tintColor;
+ } completion:nil];
+ }
+
+ // iOS 13 及以上,title 的更新只在 viewWillAppear 这里进行就可以了,但 iOS 12 及以下还要靠 popViewController 那边
+ // iOS 12 及以下系统,在不使用自定义 titleView 的情况下,在 viewWillAppear 时通过修改 navigationBar.titleTextAttributes 来设置新界面的导航栏标题样式,push 时是生效的,但 pop 时右边界面的样式会覆盖左边界面的样式,所以 pop 时的 titleTextAttributes 改为在 did pop 时处理
+ // 如果用自定义 titleView 则没这种问题,只是为了代码简单,时机的选择不区分是否自定义 title
+ [vc renderNavigationBarTitleAppearanceAnimated:animated];
}
-#pragma mark - 工具方法
-
-// 根据当前的viewController,统一处理导航栏底部的分隔线、状态栏的颜色
-- (void)renderNavigationStyleInViewController:(UIViewController *)viewController animated:(BOOL)animated {
+// 仅处理导航栏标题
+- (void)renderNavigationBarTitleAppearanceAnimated:(BOOL)animated {
// 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController
- if (![viewController.navigationController.viewControllers containsObject:viewController]) {
+ if (![self.navigationController.viewControllers containsObject:self]) {
+ return;
+ }
+
+ if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) {
return;
}
// 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式
- if ([viewController conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) {
- UIViewController *vc = (UIViewController *)viewController;
-
- // 控制界面的状态栏颜色
- BeginIgnoreDeprecatedWarning
- if ([vc respondsToSelector:@selector(shouldSetStatusBarStyleLight)] && [vc shouldSetStatusBarStyleLight]) {
- if ([[UIApplication sharedApplication] statusBarStyle] < UIStatusBarStyleLightContent) {
- [QMUIHelper renderStatusBarStyleLight];
- }
+ UIViewController *vc = (UIViewController *)self;
+ UINavigationController *navigationController = vc.navigationController;
+
+ // 导航栏title的颜色
+ if ([vc respondsToSelector:@selector(qmui_titleViewTintColor)]) {
+ UIColor *tintColor = [vc qmui_titleViewTintColor];
+ if (vc.navigationItem.titleView.qmui_useAsNavigationTitleView) {
+ vc.navigationItem.titleView.tintColor = tintColor;
+ } else if (!vc.navigationItem.titleView) {
+ NSMutableDictionary *titleTextAttributes = (navigationController.navigationBar.titleTextAttributes ?: @{}).mutableCopy;
+ titleTextAttributes[NSForegroundColorAttributeName] = tintColor;
+ navigationController.navigationBar.titleTextAttributes = titleTextAttributes.copy;
} else {
- if ([[UIApplication sharedApplication] statusBarStyle] >= UIStatusBarStyleLightContent) {
- [QMUIHelper renderStatusBarStyleDark];
- }
- }
- EndIgnoreDeprecatedWarning
-
- // 显示/隐藏 导航栏
- if ([vc canCustomNavigationBarTransitionIfBarHiddenable]) {
- if ([vc hideNavigationBarWhenTransitioning]) {
- if (!viewController.navigationController.isNavigationBarHidden) {
- [viewController.navigationController setNavigationBarHidden:YES animated:animated];
- }
- } else {
- if (viewController.navigationController.isNavigationBarHidden) {
- [viewController.navigationController setNavigationBarHidden:NO animated:animated];
- }
- }
- }
-
- // 导航栏的背景色
- if ([vc respondsToSelector:@selector(navigationBarBarTintColor)]) {
- UIColor *barTintColor = [vc navigationBarBarTintColor];
- viewController.navigationController.navigationBar.barTintColor = barTintColor;
- } else if (QMUICMIActivated) {
- viewController.navigationController.navigationBar.barTintColor = NavBarBarTintColor;
- }
-
- // 导航栏的背景
- if ([vc respondsToSelector:@selector(navigationBarBackgroundImage)]) {
- UIImage *backgroundImage = [vc navigationBarBackgroundImage];
- [viewController.navigationController.navigationBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault];
- } else if (QMUICMIActivated) {
- [viewController.navigationController.navigationBar setBackgroundImage:NavBarBackgroundImage forBarMetrics:UIBarMetricsDefault];
- }
-
- // 导航栏底部的分隔线
- if ([vc respondsToSelector:@selector(navigationBarShadowImage)]) {
- UIImage *shadowImage = [vc navigationBarShadowImage];
- [viewController.navigationController.navigationBar setShadowImage:shadowImage];
- } else if (QMUICMIActivated) {
- [viewController.navigationController.navigationBar setShadowImage:NavBarShadowImage];
- }
-
- // 导航栏上控件的主题色
- UIColor *tintColor =
- [vc respondsToSelector:@selector(navigationBarTintColor)] ? [vc navigationBarTintColor] :
- QMUICMIActivated ? NavBarTintColor : nil;
- if (tintColor) {
- // 手势从 B 返回 A 过程中,取消手势,会调用 B 的 viewWillAppear,animateAlongsideTransition 在这种情况下不会生效,所以要用 qmui_poppingByInteractivePopGestureRecognizer 针对这种情况判断。
- BOOL shouldApplyTintColorTransition = (animated && ![vc qmui_poppingByInteractivePopGestureRecognizer]);
- if (shouldApplyTintColorTransition) {
- [viewController.transitionCoordinator animateAlongsideTransition:^ (id context) {
- viewController.navigationController.navigationBar.tintColor = tintColor;
- } completion:nil];
- } else {
- viewController.navigationController.navigationBar.tintColor = tintColor;
- }
+ // 设置了自定义的 navigationItem.titleView,则不处理
}
-
- // 导航栏title的颜色
- if ([vc respondsToSelector:@selector(titleViewTintColor)]) {
- UIColor *tintColor = [vc titleViewTintColor];
- if ([vc isKindOfClass:[QMUICommonViewController class]]) {
- ((QMUICommonViewController *)vc).titleView.tintColor = tintColor;
- } else {
- // TODO: molice 对 UIViewController 也支持修改 title 颜色
- }
+ } else if (QMUICMIActivated) {
+ UIColor *tintColor = NavBarTitleColor;
+ if (vc.navigationItem.titleView.qmui_useAsNavigationTitleView) {
+ vc.navigationItem.titleView.tintColor = tintColor;
+ } else if (!vc.navigationItem.titleView) {
+ NSMutableDictionary *titleTextAttributes = (navigationController.navigationBar.titleTextAttributes ?: @{}).mutableCopy;
+ titleTextAttributes[NSForegroundColorAttributeName] = tintColor;
+ navigationController.navigationBar.titleTextAttributes = titleTextAttributes.copy;
} else {
- if (QMUICMIActivated && [vc isKindOfClass:[QMUICommonViewController class]]) {
- ((QMUICommonViewController *)vc).titleView.tintColor = NavBarTitleColor;
- } else {
- // TODO: molice 对 UIViewController 也支持修改 title 颜色
- }
+ // 设置了自定义的 navigationItem.titleView,则不处理
}
}
}
-+ (void)replaceStyleForNavigationBar:(UINavigationBar *)navbarA withNavigationBar:(UINavigationBar *)navbarB {
- navbarB.barStyle = navbarA.barStyle;
- navbarB.barTintColor = navbarA.barTintColor;
- [navbarB setShadowImage:navbarA.shadowImage];
- [navbarB setBackgroundImage:[navbarA backgroundImageForBarMetrics:UIBarMetricsDefault] forBarMetrics:UIBarMetricsDefault];
-}
-
- (BOOL)respondCustomNavigationBarTransitionIfBarHiddenable {
BOOL respondIfBarHiddenable = NO;
@@ -396,7 +469,8 @@ - (BOOL)hideNavigationBarWhenTransitioning {
return NO;
}
-- (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewController *)viewController1 secondViewController:(UIViewController *)viewController2 {
+// 对于有一个界面隐藏了导航栏的情况,我们也要做自定义的动画去干预,因为如果左右两个界面导航栏样式不同,你不去干预的话,push/pop 瞬间导航栏会变成即将显示的那个界面的样式,这不符合预期
+- (BOOL)shouldCustomTransitionAutomaticallyForOperation:(UINavigationControllerOperation)operation firstViewController:(UIViewController *)viewController1 secondViewController:(UIViewController *)viewController2 {
UIViewController *vc1 = (UIViewController *)viewController1;
UIViewController *vc2 = (UIViewController *)viewController2;
@@ -405,6 +479,22 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro
return NO;// 只处理前后两个界面都是 QMUI 系列的场景
}
+ BOOL vc1Clips = vc1.isViewLoaded && vc1.view.clipsToBounds && vc1.qmui_navigationBarMaxYInViewCoordinator < NavigationContentTopConstant;
+ BOOL vc2Clips = vc2.isViewLoaded && vc2.view.clipsToBounds && vc2.qmui_navigationBarMaxYInViewCoordinator < NavigationContentTopConstant;
+ if (vc1Clips || vc2Clips) {
+ QMUILogWarn(@"UINavigationController (NavigationBarTransition)", @"因界面布局原因导致无法优化导航栏动画,vc1 = %@,maxY1 = %.0f, vc2 = %@,maxY2 = %.0f", vc1, vc1.qmui_navigationBarMaxYInViewCoordinator, vc2, vc2.qmui_navigationBarMaxYInViewCoordinator);
+ return NO;// 左右两个界面只要其中某个界面无法完整显示 navigationBar,都不进行动画优化
+ }
+
+ if ([vc1.navigationController.delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) {
+ // 说明可能有自定义的系统转场动画
+ BOOL a = [vc1 respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:fromViewController:toViewController:)] ? [vc1 shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:operation fromViewController:vc1 toViewController:vc2] : NO;
+ BOOL b = [vc2 respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:fromViewController:toViewController:)] ? [vc2 shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:operation fromViewController:vc1 toViewController:vc2] : NO;
+ if (!a && !b) {
+ return NO;
+ }
+ }
+
if ([vc1 respondsToSelector:@selector(customNavigationBarTransitionKey)] || [vc2 respondsToSelector:@selector(customNavigationBarTransitionKey)]) {
NSString *key1 = [vc1 respondsToSelector:@selector(customNavigationBarTransitionKey)] ? [vc1 customNavigationBarTransitionKey] : nil;
NSString *key2 = [vc2 respondsToSelector:@selector(customNavigationBarTransitionKey)] ? [vc2 customNavigationBarTransitionKey] : nil;
@@ -416,8 +506,10 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro
return NO;
}
- UIImage *bg1 = [vc1 respondsToSelector:@selector(navigationBarBackgroundImage)] ? [vc1 navigationBarBackgroundImage] : [[UINavigationBar appearance] backgroundImageForBarMetrics:UIBarMetricsDefault];
- UIImage *bg2 = [vc2 respondsToSelector:@selector(navigationBarBackgroundImage)] ? [vc2 navigationBarBackgroundImage] : [[UINavigationBar appearance] backgroundImageForBarMetrics:UIBarMetricsDefault];
+
+
+ UIImage *bg1 = [vc1 respondsToSelector:@selector(qmui_navigationBarBackgroundImage)] ? [vc1 qmui_navigationBarBackgroundImage] : [UINavigationBar.qmui_appearanceConfigured backgroundImageForBarMetrics:UIBarMetricsDefault];
+ UIImage *bg2 = [vc2 respondsToSelector:@selector(qmui_navigationBarBackgroundImage)] ? [vc2 qmui_navigationBarBackgroundImage] : [UINavigationBar.qmui_appearanceConfigured backgroundImageForBarMetrics:UIBarMetricsDefault];
if (bg1 || bg2) {
if (!bg1 || !bg2) {
return YES;// 一个有一个没有,则需要自定义
@@ -427,10 +519,10 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro
}
}
- // 如果存在 backgroundImage,则 barTintColor 就算存在也不会被显示出来,所以这里只判断两个 backgroundImage 都不存在的时候
+ // 如果存在 backgroundImage,则 barTintColor、barStyle 就算存在也不会被显示出来,所以这里只判断两个 backgroundImage 都不存在的时候
if (!bg1 && !bg2) {
- UIColor *barTintColor1 = [vc1 respondsToSelector:@selector(navigationBarBarTintColor)] ? [vc1 navigationBarBarTintColor] : [UINavigationBar appearance].barTintColor;
- UIColor *barTintColor2 = [vc2 respondsToSelector:@selector(navigationBarBarTintColor)] ? [vc2 navigationBarBarTintColor] : [UINavigationBar appearance].barTintColor;
+ UIColor *barTintColor1 = [vc1 respondsToSelector:@selector(qmui_navigationBarBarTintColor)] ? [vc1 qmui_navigationBarBarTintColor] : UINavigationBar.qmui_appearanceConfigured.barTintColor;
+ UIColor *barTintColor2 = [vc2 respondsToSelector:@selector(qmui_navigationBarBarTintColor)] ? [vc2 qmui_navigationBarBarTintColor] : UINavigationBar.qmui_appearanceConfigured.barTintColor;
if (barTintColor1 || barTintColor2) {
if (!barTintColor1 || !barTintColor2) {
return YES;
@@ -439,10 +531,16 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro
return YES;
}
}
+
+ UIBarStyle barStyle1 = [vc1 respondsToSelector:@selector(qmui_navigationBarStyle)] ? [vc1 qmui_navigationBarStyle] : UINavigationBar.qmui_appearanceConfigured.barStyle;
+ UIBarStyle barStyle2 = [vc2 respondsToSelector:@selector(qmui_navigationBarStyle)] ? [vc2 qmui_navigationBarStyle] : UINavigationBar.qmui_appearanceConfigured.barStyle;
+ if (barStyle1 != barStyle2) {
+ return YES;
+ }
}
- UIImage *shadowImage1 = [vc1 respondsToSelector:@selector(navigationBarShadowImage)] ? [vc1 navigationBarShadowImage] : (vc1.navigationController.navigationBar ? vc1.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil));
- UIImage *shadowImage2 = [vc2 respondsToSelector:@selector(navigationBarShadowImage)] ? [vc2 navigationBarShadowImage] : (vc2.navigationController.navigationBar ? vc2.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil));
+ UIImage *shadowImage1 = [vc1 respondsToSelector:@selector(qmui_navigationBarShadowImage)] ? [vc1 qmui_navigationBarShadowImage] : (vc1.navigationController.navigationBar ? vc1.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil));
+ UIImage *shadowImage2 = [vc2 respondsToSelector:@selector(qmui_navigationBarShadowImage)] ? [vc2 qmui_navigationBarShadowImage] : (vc2.navigationController.navigationBar ? vc2.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil));
if (shadowImage1 || shadowImage2) {
if (!shadowImage1 || !shadowImage2) {
return YES;
@@ -456,19 +554,17 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro
}
- (UIColor *)containerViewBackgroundColor {
- UIColor *backgroundColor = UIColorWhite;
if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) {
UIViewController *vc = (UIViewController *)self;
if ([vc respondsToSelector:@selector(containerViewBackgroundColorWhenTransitioning)]) {
- backgroundColor = [vc containerViewBackgroundColorWhenTransitioning];
+ return [vc containerViewBackgroundColorWhenTransitioning];
}
}
- return backgroundColor;
+ return self.isViewLoaded && self.view.backgroundColor ? self.view.backgroundColor : UIColorWhite;
}
#pragma mark - Setter / Getter
-QMUISynthesizeBOOLProperty(lockTransitionNavigationBar, setLockTransitionNavigationBar)
QMUISynthesizeIdStrongProperty(transitionNavigationBar, setTransitionNavigationBar)
QMUISynthesizeIdStrongProperty(originContainerViewBackgroundColor, setOriginContainerViewBackgroundColor)
@@ -487,176 +583,23 @@ - (BOOL)prefersNavigationBarBackgroundViewHidden {
return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_backgroundViewHidden)) boolValue];
}
-@end
-
-
-@implementation UINavigationController (NavigationBarTransition)
-
-+ (void)load {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
-
- OverrideImplementation([UINavigationController class], @selector(pushViewController:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^(UINavigationController *selfObject, UIViewController *viewController, BOOL animated) {
-
- // call super
- void (^callSuperBlock)(UIViewController *, BOOL) = ^void(UIViewController *aViewController, BOOL aAnimated) {
- void (*originSelectorIMP)(id, SEL, UIViewController *, BOOL);
- originSelectorIMP = (void (*)(id, SEL, UIViewController *, BOOL))originalIMPProvider();
- originSelectorIMP(selfObject, originCMD, aViewController, aAnimated);
- };
-
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- if ([selfObject.delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) {
- return callSuperBlock(viewController, animated);
- }
-
- UIViewController *disappearingViewController = selfObject.viewControllers.lastObject;
- if (!disappearingViewController) {
- return callSuperBlock(viewController, animated);
- }
-
- BOOL shouldCustomNavigationBarTransition = [selfObject shouldCustomTransitionAutomaticallyWithFirstViewController:disappearingViewController secondViewController:viewController];
-
- if (shouldCustomNavigationBarTransition) {
- [disappearingViewController addTransitionNavigationBarIfNeeded];
- disappearingViewController.prefersNavigationBarBackgroundViewHidden = YES;
- }
- }
- callSuperBlock(viewController, animated);
- };
- });
-
- OverrideImplementation([UINavigationController class], @selector(setViewControllers:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^(UINavigationController *selfObject, NSArray *viewControllers, BOOL animated) {
-
- // call super
- void (^callSuperBlock)(NSArray*, BOOL) = ^void(NSArray *aViewControllers, BOOL aAnimated) {
- void (*originSelectorIMP)(id, SEL, NSArray *, BOOL);
- originSelectorIMP = (void (*)(id, SEL, NSArray *, BOOL))originalIMPProvider();
- originSelectorIMP(selfObject, originCMD, aViewControllers, aAnimated);
- };
-
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- if (viewControllers.count <= 0 || !animated) {
- return callSuperBlock(viewControllers, animated);
- }
- UIViewController *disappearingViewController = selfObject.viewControllers.lastObject;
- UIViewController *appearingViewController = viewControllers.lastObject;
- if (!disappearingViewController) {
- return callSuperBlock(viewControllers, animated);
- }
- [selfObject handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController];
- }
- callSuperBlock(viewControllers, animated);
- };
- });
-
- OverrideImplementation([UINavigationController class], @selector(popViewControllerAnimated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^UIViewController *(UINavigationController *selfObject, BOOL animated) {
-
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- UIViewController *disappearingViewController = selfObject.viewControllers.lastObject;
- UIViewController *appearingViewController = selfObject.viewControllers.count >= 2 ? selfObject.viewControllers[selfObject.viewControllers.count - 2] : nil;
- if (disappearingViewController && appearingViewController) {
- [selfObject handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController];
- }
- }
-
- // call super
- UIViewController *(*originSelectorIMP)(id, SEL, BOOL);
- originSelectorIMP = (UIViewController *(*)(id, SEL, BOOL))originalIMPProvider();
- UIViewController *result = originSelectorIMP(selfObject, originCMD, animated);
- return result;
- };
- });
-
- OverrideImplementation([UINavigationController class], @selector(popToViewController:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^NSArray *(UINavigationController *selfObject, UIViewController *viewController, BOOL animated) {
-
- // call super
- NSArray *(^callSuperBlock)(UIViewController *, BOOL) = ^NSArray*(UIViewController *aViewController, BOOL aAnimated) {
- NSArray *(*originSelectorIMP)(id, SEL, UIViewController *, BOOL);
- originSelectorIMP = (NSArray * (*)(id, SEL, UIViewController *, BOOL))originalIMPProvider();
- NSArray *result = originSelectorIMP(selfObject, originCMD, aViewController, aAnimated);
- return result;
- };
-
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- UIViewController *disappearingViewController = selfObject.viewControllers.lastObject;
- UIViewController *appearingViewController = viewController;
- NSArray *poppedViewControllers = callSuperBlock(viewController, animated);
- if (poppedViewControllers) {
- [selfObject handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController];
- }
- return poppedViewControllers;
- }
- return callSuperBlock(viewController, animated);
- };
- });
-
- ExtendImplementationOfNonVoidMethodWithSingleArgument([UINavigationController class], @selector(popToRootViewControllerAnimated:), BOOL, NSArray *, ^NSArray *(UINavigationController *selfObject, BOOL animated, NSArray *originReturnValue) {
- if (selfObject.viewControllers.count > 1) {
- UIViewController *disappearingViewController = selfObject.viewControllers.lastObject;
- UIViewController *appearingViewController = selfObject.viewControllers.firstObject;
- if (originReturnValue) {
- [selfObject handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController];
- }
- }
- return originReturnValue;
- });
- });
-}
-
-- (void)handlePopViewControllerNavigationBarTransitionWithDisappearViewController:(UIViewController *)disappearViewController appearViewController:(UIViewController *)appearViewController {
-
- if (![self.delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) {
-
- BOOL shouldCustomNavigationBarTransition = [self shouldCustomTransitionAutomaticallyWithFirstViewController:disappearViewController secondViewController:appearViewController];
-
- if (shouldCustomNavigationBarTransition) {
- [disappearViewController addTransitionNavigationBarIfNeeded];
- if (appearViewController.transitionNavigationBar) {
- // 假设从A→B→C,其中A设置了bar的样式,B跟随A所以B里没有设置bar样式的代码,C又把样式改为另一种,此时从C返回B时,由于B没有设置bar的样式的代码,所以bar的样式依然会保留C的,这就错了,所以每次都要手动改回来才保险
- [UIViewController replaceStyleForNavigationBar:appearViewController.transitionNavigationBar withNavigationBar:self.navigationBar];
- }
- disappearViewController.prefersNavigationBarBackgroundViewHidden = YES;
+static char kAssociatedObjectKey_shouldShowTransitionBar;
+- (void)setQmuinb_shouldShowTransitionBar:(BOOL)shouldShowTransitionBar {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowTransitionBar, @(shouldShowTransitionBar), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (shouldShowTransitionBar) {
+ [self addTransitionNavigationBarAndBindNavigationBar:NO];// 这里不绑定 bar,因为不知道此时是两个 vc 里的哪一个
+ self.prefersNavigationBarBackgroundViewHidden = YES;
+ } else {
+ [self removeTransitionNavigationBar];
+ // 屏蔽一些 childViewController 触发的场景,只关心堆栈里的
+ if ([self.navigationController.viewControllers containsObject:self]) {
+ self.prefersNavigationBarBackgroundViewHidden = NO;
}
-
}
}
-@end
-
-@interface UISearchController (NavigationBarTransition)
-
-@end
-
-@implementation UISearchController (NavigationBarTransition)
-
-+ (void)load {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
-
- // 修复 UISearchController push 到导航栏隐藏的界面时,会强制把导航栏重新显示出来的 bug
- // https://github.com/Tencent/QMUI_iOS/issues/479
- SEL selector = NSSelectorFromString([NSString stringWithFormat:@"_%@%@:", @"navigationController", @"WillShowViewController"]);
- NSAssert([[self class] instancesRespondToSelector:selector], @"iOS 版本更新导致 UISearchController 无法响应方法 %@", NSStringFromSelector(selector));
- OverrideImplementation([self class], selector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^(UISearchController *selfObject, NSNotification *firstArgv) {
- UIViewController *nextViewController = firstArgv.userInfo[@"UINavigationControllerNextVisibleViewController"];
- if (![selfObject isKindOfClass:originClass] || ![nextViewController canCustomNavigationBarTransitionIfBarHiddenable]) {
- void (*originSelectorIMP)(id, SEL, NSNotification *);
- originSelectorIMP = (void (*)(id, SEL, NSNotification *))originalIMPProvider();
- originSelectorIMP(selfObject, originCMD, firstArgv);
- }
- };
- });
- });
+- (BOOL)qmuinb_shouldShowTransitionBar {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_shouldShowTransitionBar)) boolValue];
}
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h
index f48b5530..ca1a82be 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAlertController.h
@@ -93,7 +93,7 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) {
UIView *_scrollWrapView; // 包含上下两个 scrollView 的容器
UIScrollView *_headerScrollView; // 上半部分的内容的 scrollView,例如 title、message
UIScrollView *_buttonScrollView; // 所有按钮的容器,特别的,actionSheet 下的取消按钮不放在这里面,因为它不参与滚动
- UIControl *_maskView; // 背后占满整个屏幕的半透明黑色遮罩
+ UIControl *_dimmingView; // 背后占满整个屏幕的半透明黑色遮罩
}
/// alert距离屏幕四边的间距,默认UIEdgeInsetsMake(0, 0, 0, 0)。alert的宽度最终是通过屏幕宽度减去水平的 alertContentMargin 和 alertContentMaximumWidth 决定的。
@@ -153,6 +153,12 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) {
/// alert 内部 textField 的边框颜色,如果不需要边框,可设置为 nil
@property(nullable, nonatomic, strong) UIColor *alertTextFieldBorderColor UI_APPEARANCE_SELECTOR;
+/// alert 内部 textField 的 textInsets,textField 的高度会由文字大小加这个 inset 来决定
+@property(nonatomic, assign) UIEdgeInsets alertTextFieldTextInsets UI_APPEARANCE_SELECTOR;
+
+/// alert 内部 textField 的 margin,当存在多个 textField 时可通过参数 @c aTextFieldIndex 来为不同 textField 设置不一样的 margin。
+/// @note 注意 margin 是在原有布局基础上叠加的,左右叠加 @c alertHeaderInsets ,顶部 @c alertHeaderInsets.top ,底部为 0。
+@property(nonatomic, copy) UIEdgeInsets (^alertTextFieldMarginBlock)(__kindof QMUIAlertController *aAlertController, NSInteger aTextFieldIndex);
/// sheet距离屏幕四边的间距,默认UIEdgeInsetsMake(10, 10, 10, 10)。
@property(nonatomic, assign) UIEdgeInsets sheetContentMargin UI_APPEARANCE_SELECTOR;
@@ -205,6 +211,8 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) {
/// sheet头部title和message之间的间距,默认8pt
@property(nonatomic, assign) CGFloat sheetTitleMessageSpacing UI_APPEARANCE_SELECTOR;
+/// sheet 的列数,一行显示多少个 button,默认是 1。
+@property(nonatomic, assign) CGFloat sheetButtonColumnCount UI_APPEARANCE_SELECTOR;
/// 默认初始化方法
- (nonnull instancetype)initWithTitle:(nullable NSString *)title message:(nullable NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle;
@@ -257,10 +265,10 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) {
/// 将`QMUIAlertController`弹出来的`QMUIModalPresentationViewController`对象
@property(nullable, nonatomic, strong, readonly) QMUIModalPresentationViewController *modalPresentationViewController;
-/// 主体内容(alert 下指整个弹窗,actionSheet 下指取消按钮上方的那些 header 和 按钮)背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它(但推荐用 QMUIVisualEffectView)。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。
+/// 主体内容(alert 下指整个弹窗,actionSheet 下指取消按钮上方的那些 header 和 按钮)背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。
@property(null_resettable, nonatomic, strong) UIView *mainVisualEffectView;
-/// actionSheet 下的取消按钮背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它(但推荐用 QMUIVisualEffectView)。alert 情况下不会出现。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。
+/// actionSheet 下的取消按钮背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它。alert 情况下不会出现。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。
@property(null_resettable, nonatomic, strong) UIView *cancelButtonVisualEffectView;
/**
@@ -270,16 +278,13 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) {
*/
@property(nonatomic, assign) BOOL orderActionsByAddedOrdered;
-/// maskView是否响应点击,alert默认为NO,sheet默认为YES
-@property(nonatomic, assign) BOOL shouldRespondMaskViewTouch;
+/// dimmingView 是否响应点击,alert 默认为NO,sheet 默认为YES
+@property(nonatomic, assign) BOOL shouldRespondDimmingViewTouch;
/// 在 iPhoneX 机器上是否延伸底部背景色。因为在 iPhoneX 上我们会把整个面板往上移动 safeArea 的距离,如果你的面板本来就配置成撑满全屏的样式,那么就会露出底部的空隙,isExtendBottomLayout 可以帮助你把空暇填补上。默认为NO。
/// @warning: 只对 sheet 类型有效
@property(nonatomic, assign) BOOL isExtendBottomLayout UI_APPEARANCE_SELECTOR;
-/// 在显示 alert 之前先降下键盘,默认为 YES。系统的 UIAlertController 也会在显示时降下键盘,但它能在消失后把键盘自动升起,并且这个过程不会触发 becomeFirstResponder/resignFirstResponder,QMUIAlertController 暂时做不到这样的效果,只负责降下,不负责恢复。
-@property(nonatomic, assign) BOOL dismissKeyboardAutomatically;
-
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m
index a84f74d1..32a201a2 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIAlertController.m
@@ -23,6 +23,8 @@
#import "UIImage+QMUI.h"
#import "CALayer+QMUI.h"
#import "QMUIKeyboardManager.h"
+#import "QMUIAppearance.h"
+#import "QMUILabel.h"
static NSUInteger alertControllerCount = 0;
@@ -75,7 +77,7 @@ @interface QMUIAlertAction ()
@implementation QMUIAlertAction
+ (nonnull instancetype)actionWithTitle:(nullable NSString *)title style:(QMUIAlertActionStyle)style handler:(void (^)(__kindof QMUIAlertController *, QMUIAlertAction *))handler {
- QMUIAlertAction *alertAction = [[QMUIAlertAction alloc] init];
+ QMUIAlertAction *alertAction = [[self alloc] init];
alertAction.title = title;
alertAction.style = style;
alertAction.handler = handler;
@@ -111,66 +113,59 @@ - (void)handleAlertActionEvent:(id)sender {
@implementation QMUIAlertController (UIAppearance)
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self appearance];
- });
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
}
-static QMUIAlertController *alertControllerAppearance;
-+ (nonnull instancetype)appearance {
++ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- [self resetAppearance];
+ [self initAppearance];
});
- return alertControllerAppearance;
}
-+ (void)resetAppearance {
- if (!alertControllerAppearance) {
-
- alertControllerAppearance = [[QMUIAlertController alloc] init];
-
- alertControllerAppearance.alertContentMargin = UIEdgeInsetsMake(0, 0, 0, 0);
- alertControllerAppearance.alertContentMaximumWidth = 270;
- alertControllerAppearance.alertSeparatorColor = UIColorMake(211, 211, 219);
- alertControllerAppearance.alertTitleAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontBoldMake(17),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]};
- alertControllerAppearance.alertMessageAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]};
- alertControllerAppearance.alertButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)};
- alertControllerAppearance.alertButtonDisabledAttributes = @{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)};
- alertControllerAppearance.alertCancelButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(17),NSKernAttributeName:@(0)};
- alertControllerAppearance.alertDestructiveButtonAttributes = @{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)};
- alertControllerAppearance.alertContentCornerRadius = 13;
- alertControllerAppearance.alertButtonHeight = 44;
- alertControllerAppearance.alertHeaderBackgroundColor = UIColorMakeWithRGBA(247, 247, 247, 1);
- alertControllerAppearance.alertButtonBackgroundColor = alertControllerAppearance.alertHeaderBackgroundColor;
- alertControllerAppearance.alertButtonHighlightBackgroundColor = UIColorMake(232, 232, 232);
- alertControllerAppearance.alertHeaderInsets = UIEdgeInsetsMake(20, 16, 20, 16);
- alertControllerAppearance.alertTitleMessageSpacing = 3;
- alertControllerAppearance.alertTextFieldFont = UIFontMake(14);
- alertControllerAppearance.alertTextFieldTextColor = UIColorBlack;
- alertControllerAppearance.alertTextFieldBorderColor = UIColorMake(210, 210, 210);
-
- alertControllerAppearance.sheetContentMargin = UIEdgeInsetsMake(10, 10, 10, 10);
- alertControllerAppearance.sheetContentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(alertControllerAppearance.sheetContentMargin);
- alertControllerAppearance.sheetSeparatorColor = UIColorMake(211, 211, 219);
- alertControllerAppearance.sheetTitleAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontBoldMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]};
- alertControllerAppearance.sheetMessageAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]};
- alertControllerAppearance.sheetButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)};
- alertControllerAppearance.sheetButtonDisabledAttributes = @{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)};
- alertControllerAppearance.sheetCancelButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(20),NSKernAttributeName:@(0)};
- alertControllerAppearance.sheetDestructiveButtonAttributes = @{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)};
- alertControllerAppearance.sheetCancelButtonMarginTop = 8;
- alertControllerAppearance.sheetContentCornerRadius = 13;
- alertControllerAppearance.sheetButtonHeight = 57;
- alertControllerAppearance.sheetHeaderBackgroundColor = UIColorMakeWithRGBA(247, 247, 247, 1);
- alertControllerAppearance.sheetButtonBackgroundColor = alertControllerAppearance.sheetHeaderBackgroundColor;
- alertControllerAppearance.sheetButtonHighlightBackgroundColor = UIColorMake(232, 232, 232);
- alertControllerAppearance.sheetHeaderInsets = UIEdgeInsetsMake(16, 16, 16, 16);
- alertControllerAppearance.sheetTitleMessageSpacing = 8;
- alertControllerAppearance.isExtendBottomLayout = NO;
- }
++ (void)initAppearance {
+ QMUIAlertController *alertControllerAppearance = QMUIAlertController.appearance;
+ alertControllerAppearance.alertContentMargin = UIEdgeInsetsMake(0, 0, 0, 0);
+ alertControllerAppearance.alertContentMaximumWidth = 270;
+ alertControllerAppearance.alertSeparatorColor = UIColorMake(211, 211, 219);
+ alertControllerAppearance.alertTitleAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontBoldMake(17),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]};
+ alertControllerAppearance.alertMessageAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]};
+ alertControllerAppearance.alertButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)};
+ alertControllerAppearance.alertButtonDisabledAttributes = @{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)};
+ alertControllerAppearance.alertCancelButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(17),NSKernAttributeName:@(0)};
+ alertControllerAppearance.alertDestructiveButtonAttributes = @{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)};
+ alertControllerAppearance.alertContentCornerRadius = 13;
+ alertControllerAppearance.alertButtonHeight = 44;
+ alertControllerAppearance.alertHeaderBackgroundColor = UIColorMakeWithRGBA(247, 247, 247, 1);
+ alertControllerAppearance.alertButtonBackgroundColor = alertControllerAppearance.alertHeaderBackgroundColor;
+ alertControllerAppearance.alertButtonHighlightBackgroundColor = UIColorMake(232, 232, 232);
+ alertControllerAppearance.alertHeaderInsets = UIEdgeInsetsMake(20, 16, 20, 16);
+ alertControllerAppearance.alertTitleMessageSpacing = 3;
+ alertControllerAppearance.alertTextFieldFont = UIFontMake(14);
+ alertControllerAppearance.alertTextFieldTextColor = UIColorBlack;
+ alertControllerAppearance.alertTextFieldBorderColor = UIColorMake(210, 210, 210);
+ alertControllerAppearance.alertTextFieldTextInsets = UIEdgeInsetsMake(4, 7, 4, 7);
+
+ alertControllerAppearance.sheetContentMargin = UIEdgeInsetsMake(10, 10, 10, 10);
+ alertControllerAppearance.sheetContentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(alertControllerAppearance.sheetContentMargin);
+ alertControllerAppearance.sheetSeparatorColor = UIColorMake(211, 211, 219);
+ alertControllerAppearance.sheetTitleAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontBoldMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]};
+ alertControllerAppearance.sheetMessageAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]};
+ alertControllerAppearance.sheetButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)};
+ alertControllerAppearance.sheetButtonDisabledAttributes = @{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)};
+ alertControllerAppearance.sheetCancelButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(20),NSKernAttributeName:@(0)};
+ alertControllerAppearance.sheetDestructiveButtonAttributes = @{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)};
+ alertControllerAppearance.sheetCancelButtonMarginTop = 8;
+ alertControllerAppearance.sheetContentCornerRadius = 13;
+ alertControllerAppearance.sheetButtonHeight = 57;
+ alertControllerAppearance.sheetHeaderBackgroundColor = UIColorMakeWithRGBA(247, 247, 247, 1);
+ alertControllerAppearance.sheetButtonBackgroundColor = alertControllerAppearance.sheetHeaderBackgroundColor;
+ alertControllerAppearance.sheetButtonHighlightBackgroundColor = UIColorMake(232, 232, 232);
+ alertControllerAppearance.sheetHeaderInsets = UIEdgeInsetsMake(16, 16, 16, 16);
+ alertControllerAppearance.sheetTitleMessageSpacing = 8;
+ alertControllerAppearance.sheetButtonColumnCount = 1;
+ alertControllerAppearance.isExtendBottomLayout = NO;
}
@end
@@ -185,7 +180,7 @@ @interface QMUIAlertController () *alertActions;
@@ -237,49 +232,14 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {
}
- (void)didInitialize {
- if (alertControllerAppearance) {
- self.alertContentMargin = [QMUIAlertController appearance].alertContentMargin;
- self.alertContentMaximumWidth = [QMUIAlertController appearance].alertContentMaximumWidth;
- self.alertSeparatorColor = [QMUIAlertController appearance].alertSeparatorColor;
- self.alertContentCornerRadius = [QMUIAlertController appearance].alertContentCornerRadius;
- self.alertTitleAttributes = [QMUIAlertController appearance].alertTitleAttributes;
- self.alertMessageAttributes = [QMUIAlertController appearance].alertMessageAttributes;
- self.alertButtonAttributes = [QMUIAlertController appearance].alertButtonAttributes;
- self.alertButtonDisabledAttributes = [QMUIAlertController appearance].alertButtonDisabledAttributes;
- self.alertCancelButtonAttributes = [QMUIAlertController appearance].alertCancelButtonAttributes;
- self.alertDestructiveButtonAttributes = [QMUIAlertController appearance].alertDestructiveButtonAttributes;
- self.alertButtonHeight = [QMUIAlertController appearance].alertButtonHeight;
- self.alertHeaderBackgroundColor = [QMUIAlertController appearance].alertHeaderBackgroundColor;
- self.alertButtonBackgroundColor = [QMUIAlertController appearance].alertButtonBackgroundColor;
- self.alertButtonHighlightBackgroundColor = [QMUIAlertController appearance].alertButtonHighlightBackgroundColor;
- self.alertHeaderInsets = [QMUIAlertController appearance].alertHeaderInsets;
- self.alertTitleMessageSpacing = [QMUIAlertController appearance].alertTitleMessageSpacing;
- self.alertTextFieldFont = [QMUIAlertController appearance].alertTextFieldFont;
- self.alertTextFieldTextColor = [QMUIAlertController appearance].alertTextFieldTextColor;
- self.alertTextFieldBorderColor = [QMUIAlertController appearance].alertTextFieldBorderColor;
-
- self.sheetContentMargin = [QMUIAlertController appearance].sheetContentMargin;
- self.sheetContentMaximumWidth = [QMUIAlertController appearance].sheetContentMaximumWidth;
- self.sheetSeparatorColor = [QMUIAlertController appearance].sheetSeparatorColor;
- self.sheetTitleAttributes = [QMUIAlertController appearance].sheetTitleAttributes;
- self.sheetMessageAttributes = [QMUIAlertController appearance].sheetMessageAttributes;
- self.sheetButtonAttributes = [QMUIAlertController appearance].sheetButtonAttributes;
- self.sheetButtonDisabledAttributes = [QMUIAlertController appearance].sheetButtonDisabledAttributes;
- self.sheetCancelButtonAttributes = [QMUIAlertController appearance].sheetCancelButtonAttributes;
- self.sheetDestructiveButtonAttributes = [QMUIAlertController appearance].sheetDestructiveButtonAttributes;
- self.sheetCancelButtonMarginTop = [QMUIAlertController appearance].sheetCancelButtonMarginTop;
- self.sheetContentCornerRadius = [QMUIAlertController appearance].sheetContentCornerRadius;
- self.sheetButtonHeight = [QMUIAlertController appearance].sheetButtonHeight;
- self.sheetHeaderBackgroundColor = [QMUIAlertController appearance].sheetHeaderBackgroundColor;
- self.sheetButtonBackgroundColor = [QMUIAlertController appearance].sheetButtonBackgroundColor;
- self.sheetButtonHighlightBackgroundColor = [QMUIAlertController appearance].sheetButtonHighlightBackgroundColor;
- self.sheetHeaderInsets = [QMUIAlertController appearance].sheetHeaderInsets;
- self.sheetTitleMessageSpacing = [QMUIAlertController appearance].sheetTitleMessageSpacing;
- self.isExtendBottomLayout = [QMUIAlertController appearance].isExtendBottomLayout;
- }
-
+ [self qmui_applyAppearance];
+ self.alertTextFieldMarginBlock = ^UIEdgeInsets(__kindof QMUIAlertController *aAlertController, NSInteger aTextFieldIndex) {
+ if (aTextFieldIndex == aAlertController.textFields.count - 1) {
+ return UIEdgeInsetsMake(0, 0, 16, 0);
+ }
+ return UIEdgeInsetsZero;
+ };
self.shouldManageTextFieldsReturnEventAutomatically = YES;
- self.dismissKeyboardAutomatically = YES;
}
- (void)setAlertButtonAttributes:(NSDictionary *)alertButtonAttributes {
@@ -375,9 +335,9 @@ - (void)setSheetHeaderBackgroundColor:(UIColor *)sheetHeaderBackgroundColor {
- (void)updateHeaderBackgrondColor {
if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) {
- if (self.headerScrollView) { self.headerScrollView.backgroundColor = self.sheetHeaderBackgroundColor; }
+ if (_headerScrollView) { _headerScrollView.backgroundColor = self.sheetHeaderBackgroundColor; }
} else if (self.preferredStyle == QMUIAlertControllerStyleAlert) {
- if (self.headerScrollView) { self.headerScrollView.backgroundColor = self.alertHeaderBackgroundColor; }
+ if (_headerScrollView) { _headerScrollView.backgroundColor = self.alertHeaderBackgroundColor; }
}
}
@@ -419,15 +379,15 @@ - (void)setIsExtendBottomLayout:(BOOL)isExtendBottomLayout {
}
- (void)updateExtendLayerAppearance {
- if (self.extendLayer) {
- self.extendLayer.backgroundColor = self.sheetButtonBackgroundColor.CGColor;
+ if (_extendLayer) {
+ _extendLayer.backgroundColor = self.sheetButtonBackgroundColor.CGColor;
}
}
- (void)updateCornerRadius {
if (self.preferredStyle == QMUIAlertControllerStyleAlert) {
if (self.containerView) { self.containerView.layer.cornerRadius = self.alertContentCornerRadius; self.containerView.clipsToBounds = YES; }
- if (self.cancelButtonVisualEffectView) { self.cancelButtonVisualEffectView.layer.cornerRadius = 0; self.cancelButtonVisualEffectView.clipsToBounds = NO;}
+ if (self.cancelButtonVisualEffectView) { self.cancelButtonVisualEffectView.layer.cornerRadius = self.alertContentCornerRadius; self.cancelButtonVisualEffectView.clipsToBounds = NO;}
if (self.scrollWrapView) { self.scrollWrapView.layer.cornerRadius = 0; self.scrollWrapView.clipsToBounds = NO; }
} else {
if (self.containerView) { self.containerView.layer.cornerRadius = 0; self.containerView.clipsToBounds = NO; }
@@ -457,6 +417,20 @@ - (void)setAlertTextFieldTextColor:(UIColor *)alertTextFieldTextColor {
}];
}
+- (void)setAlertTextFieldTextInsets:(UIEdgeInsets)alertTextFieldTextInsets {
+ _alertTextFieldTextInsets = alertTextFieldTextInsets;
+ [self.textFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) {
+ textField.textInsets = alertTextFieldTextInsets;
+ }];
+}
+
+- (void)setAlertTextFieldMarginBlock:(UIEdgeInsets (^)(__kindof QMUIAlertController * _Nonnull, NSInteger))alertTextFieldMarginBlock {
+ _alertTextFieldMarginBlock = alertTextFieldMarginBlock;
+ if (self.isViewLoaded) {
+ [self.view setNeedsLayout];
+ }
+}
+
- (void)setMainVisualEffectView:(UIView *)mainVisualEffectView {
if (!mainVisualEffectView) {
// 不允许为空
@@ -524,41 +498,17 @@ - (nonnull instancetype)initWithTitle:(nullable NSString *)title message:(nullab
self.preferredStyle = preferredStyle;
- self.shouldRespondMaskViewTouch = preferredStyle == QMUIAlertControllerStyleActionSheet;
+ self.shouldRespondDimmingViewTouch = preferredStyle == QMUIAlertControllerStyleActionSheet;
self.alertActions = [[NSMutableArray alloc] init];
self.alertTextFields = [[NSMutableArray alloc] init];
self.destructiveActions = [[NSMutableArray alloc] init];
- self.containerView = [[UIView alloc] init];
-
- self.maskView = [[UIControl alloc] init];
- self.maskView.alpha = 0;
- self.maskView.backgroundColor = UIColorMask;
- [self.maskView addTarget:self action:@selector(handleMaskViewEvent:) forControlEvents:UIControlEventTouchUpInside];
-
- self.scrollWrapView = [[UIView alloc] init];
- self.mainVisualEffectView = [[UIView alloc] init];
- self.cancelButtonVisualEffectView = [[UIView alloc] init];
- self.headerScrollView = [[UIScrollView alloc] init];
- self.headerScrollView.scrollsToTop = NO;
- self.buttonScrollView = [[UIScrollView alloc] init];
- self.buttonScrollView.scrollsToTop = NO;
- if (@available(iOS 11, *)) {
- self.headerScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- self.buttonScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- }
-
- self.extendLayer = [CALayer layer];
- self.extendLayer.hidden = !self.isExtendBottomLayout;
- [self.extendLayer qmui_removeDefaultAnimations];
-
self.title = title;
self.message = message;
- [self updateHeaderBackgrondColor];
- [self updateExtendLayerAppearance];
-
+ self.mainVisualEffectView = [[UIView alloc] init];
+ self.cancelButtonVisualEffectView = [[UIView alloc] init];
}
return self;
}
@@ -569,7 +519,8 @@ - (QMUIAlertControllerStyle)preferredStyle {
- (void)viewDidLoad {
[super viewDidLoad];
- [self.view addSubview:self.maskView];
+
+ [self.view addSubview:self.dimmingView];
[self.view addSubview:self.containerView];
[self.containerView addSubview:self.scrollWrapView];
[self.scrollWrapView addSubview:self.headerScrollView];
@@ -585,9 +536,10 @@ - (void)viewDidLayoutSubviews {
BOOL hasMessage = (self.messageLabel.text.length > 0 && !self.messageLabel.hidden);
BOOL hasTextField = self.alertTextFields.count > 0;
BOOL hasCustomView = !!_customView;
+ BOOL shouldShowSeparatorAtTopOfButtonAtFirstLine = hasTitle || hasMessage || hasCustomView;
CGFloat contentOriginY = 0;
- self.maskView.frame = self.view.bounds;
+ self.dimmingView.frame = self.view.bounds;
if (self.preferredStyle == QMUIAlertControllerStyleAlert) {
@@ -613,10 +565,17 @@ - (void)viewDidLayoutSubviews {
if (hasTextField) {
for (int i = 0; i < self.alertTextFields.count; i++) {
UITextField *textField = self.alertTextFields[i];
- textField.frame = CGRectMake(contentPaddingLeft, contentOriginY, CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight, 25);
- contentOriginY = CGRectGetMaxY(textField.frame) - 1;
+ CGRect textFieldFrame = CGRectMake(contentPaddingLeft, contentOriginY, CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight, CGFLOAT_MAX);
+ CGSize textFieldSize = [textField sizeThatFits:textFieldFrame.size];
+ textFieldFrame = CGRectSetHeight(textFieldFrame, textFieldSize.height);
+ UIEdgeInsets margin = UIEdgeInsetsZero;
+ if (self.alertTextFieldMarginBlock) {
+ margin = self.alertTextFieldMarginBlock(self, i);
+ }
+ textFieldFrame = CGRectMake(CGRectGetMinX(textFieldFrame) + margin.left, CGRectGetMinY(textFieldFrame) + margin.top, CGRectGetWidth(textFieldFrame) - UIEdgeInsetsGetHorizontalValue(margin), CGRectGetHeight(textFieldFrame));
+ contentOriginY = CGRectGetMaxY(textFieldFrame) + margin.bottom - textField.layer.borderWidth;
+ textField.frame = textFieldFrame;
}
- contentOriginY += 16;
}
// 自定义view的布局 - 自动居中
if (hasCustomView) {
@@ -648,16 +607,21 @@ - (void)viewDidLayoutSubviews {
// 对齐系统,先 add 的在右边,后 add 的在左边
QMUIAlertAction *leftAction = newOrderActions[1];
leftAction.button.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.buttonScrollView.bounds) / 2, self.alertButtonHeight);
- leftAction.button.qmui_borderPosition = QMUIViewBorderPositionTop|QMUIViewBorderPositionRight;
+ leftAction.button.qmui_borderPosition = QMUIViewBorderPositionRight;
QMUIAlertAction *rightAction = newOrderActions[0];
rightAction.button.frame = CGRectMake(CGRectGetMaxX(leftAction.button.frame), contentOriginY, CGRectGetWidth(self.buttonScrollView.bounds) / 2, self.alertButtonHeight);
- rightAction.button.qmui_borderPosition = QMUIViewBorderPositionTop;
+ if (shouldShowSeparatorAtTopOfButtonAtFirstLine) {
+ leftAction.button.qmui_borderPosition |= QMUIViewBorderPositionTop;
+ rightAction.button.qmui_borderPosition = QMUIViewBorderPositionTop;
+ }
contentOriginY = CGRectGetMaxY(leftAction.button.frame);
} else {
for (int i = 0; i < newOrderActions.count; i++) {
QMUIAlertAction *action = newOrderActions[i];
action.button.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.containerView.bounds), self.alertButtonHeight);
- action.button.qmui_borderPosition = QMUIViewBorderPositionTop;
+ if (i > 0 || shouldShowSeparatorAtTopOfButtonAtFirstLine) {
+ action.button.qmui_borderPosition = QMUIViewBorderPositionTop;
+ }
contentOriginY = CGRectGetMaxY(action.button.frame);
}
}
@@ -667,7 +631,7 @@ - (void)viewDidLayoutSubviews {
self.buttonScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.buttonScrollView.bounds), contentOriginY);
// 容器最后布局
CGFloat contentHeight = CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds);
- CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds);
+ CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds) - UIEdgeInsetsGetVerticalValue(SafeAreaInsetsConstantForDeviceWithNotch) - self.keyboardHeight;
if (contentHeight > screenSpaceHeight - 20) {
screenSpaceHeight -= 20;
CGFloat contentH = fmin(CGRectGetHeight(self.headerScrollView.bounds), screenSpaceHeight / 2);
@@ -691,8 +655,7 @@ - (void)viewDidLayoutSubviews {
self.scrollWrapView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), contentHeight);
self.mainVisualEffectView.frame = self.scrollWrapView.bounds;
- CGRect containerRect = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.bounds)) / 2, (screenSpaceHeight - contentHeight - self.keyboardHeight) / 2, CGRectGetWidth(self.containerView.bounds), CGRectGetHeight(self.scrollWrapView.bounds));
- self.containerView.frame = CGRectFlatted(CGRectApplyAffineTransform(containerRect, self.containerView.transform));
+ self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, SafeAreaInsetsConstantForDeviceWithNotch.top + (screenSpaceHeight - contentHeight) / 2, CGRectGetWidth(self.containerView.frame), CGRectGetHeight(self.scrollWrapView.bounds));
}
else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) {
@@ -727,18 +690,58 @@ - (void)viewDidLayoutSubviews {
contentOriginY = CGRectGetMaxY(self.headerScrollView.frame);
// 按钮的布局
self.buttonScrollView.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.containerView.bounds), 0);
- contentOriginY = 0;
NSArray *newOrderActions = [self orderedAlertActions:self.alertActions];
+ if (self.sheetButtonColumnCount > 1) {
+ // 如果是多列,则为了布局,补齐 item 个数
+ NSMutableArray *fixedActions = [newOrderActions mutableCopy];
+ [fixedActions removeObject:self.cancelAction];
+
+ if (fmodf(fixedActions.count, self.sheetButtonColumnCount) != 0) {
+ NSInteger increment = self.sheetButtonColumnCount - fmodf(fixedActions.count, self.sheetButtonColumnCount);
+ for (NSInteger i = 0; i < increment; i++) {
+ QMUIAlertAction *action = [[QMUIAlertAction alloc] init];
+ action.title = @"";
+ action.style = QMUIAlertActionStyleDefault;
+ action.handler = nil;
+ [self.buttonScrollView addSubview:action.button];
+ [fixedActions addObject:action];
+ }
+
+ [fixedActions addObject:self.cancelAction];
+ newOrderActions = [fixedActions copy];
+ }
+ }
+
+ CGFloat columnCount = self.sheetButtonColumnCount;
+ CGFloat alertActionsWidth = CGRectGetWidth(self.buttonScrollView.bounds) / columnCount;
+ CGFloat alertActionsLayoutX = 0;
+ CGFloat alertActionsLayoutY = 0;
+ contentOriginY = 0;
if (self.alertActions.count > 0) {
- contentOriginY = (hasTitle || hasMessage || hasCustomView) ? contentOriginY : contentOriginY;
for (int i = 0; i < newOrderActions.count; i++) {
QMUIAlertAction *action = newOrderActions[i];
if (action.style == QMUIAlertActionStyleCancel && i == newOrderActions.count - 1) {
continue;
} else {
- action.button.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.buttonScrollView.bounds), self.sheetButtonHeight);
- action.button.qmui_borderPosition = QMUIViewBorderPositionTop;
- contentOriginY = CGRectGetMaxY(action.button.frame);
+ BOOL isFirstLine = floor(i / columnCount) == 0;
+ BOOL isLastColumn = fmod(i + 1, columnCount) == 0;
+ BOOL shouldShowSeparatorAtTop = !isFirstLine || shouldShowSeparatorAtTopOfButtonAtFirstLine;
+ BOOL shouldShowSeparatorAtRight = !isLastColumn;// 单列时全都不用显示右分隔线,多列时最后一列不用显示右分隔线
+ action.button.frame = CGRectMake(alertActionsLayoutX, alertActionsLayoutY, alertActionsWidth, self.sheetButtonHeight);
+ if (isLastColumn) {
+ alertActionsLayoutX = 0;
+ alertActionsLayoutY = CGRectGetMaxY(action.button.frame);
+ } else {
+ alertActionsLayoutX += alertActionsWidth;
+ }
+ contentOriginY = MAX(contentOriginY, CGRectGetMaxY(action.button.frame));
+
+ if (shouldShowSeparatorAtTop) {
+ action.button.qmui_borderPosition |= QMUIViewBorderPositionTop;
+ }
+ if (shouldShowSeparatorAtRight) {
+ action.button.qmui_borderPosition |= QMUIViewBorderPositionRight;
+ }
}
}
}
@@ -756,7 +759,7 @@ - (void)viewDidLayoutSubviews {
}
// 把上下的margin都加上用于跟整个屏幕的高度做比较
CGFloat contentHeight = contentOriginY + UIEdgeInsetsGetVerticalValue(self.sheetContentMargin);
- CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds);
+ CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds) - SafeAreaInsetsConstantForDeviceWithNotch.top - (self.isExtendBottomLayout ? 0 : SafeAreaInsetsConstantForDeviceWithNotch.bottom);
if (contentHeight > screenSpaceHeight) {
CGFloat cancelButtonAreaHeight = (self.cancelAction ? (CGRectGetHeight(self.cancelAction.button.bounds) + self.sheetCancelButtonMarginTop) : 0);
screenSpaceHeight = screenSpaceHeight - cancelButtonAreaHeight - UIEdgeInsetsGetVerticalValue(self.sheetContentMargin);
@@ -786,8 +789,7 @@ - (void)viewDidLayoutSubviews {
contentHeight -= self.sheetContentMargin.top;
}
- CGRect containerRect = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.bounds)) / 2, screenSpaceHeight - contentHeight - SafeAreaInsetsConstantForDeviceWithNotch.bottom, CGRectGetWidth(self.containerView.bounds), contentHeight + (self.isExtendBottomLayout ? SafeAreaInsetsConstantForDeviceWithNotch.bottom : 0));
- self.containerView.frame = CGRectFlatted(CGRectApplyAffineTransform(containerRect, self.containerView.transform));
+ self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, SafeAreaInsetsConstantForDeviceWithNotch.top + screenSpaceHeight - contentHeight, CGRectGetWidth(self.containerView.frame), contentHeight + (self.isExtendBottomLayout ? SafeAreaInsetsConstantForDeviceWithNotch.bottom : 0));
self.extendLayer.frame = CGRectFlatMake(0, CGRectGetHeight(self.containerView.bounds) - SafeAreaInsetsConstantForDeviceWithNotch.bottom - 1, CGRectGetWidth(self.containerView.bounds), SafeAreaInsetsConstantForDeviceWithNotch.bottom + 1);
}
@@ -843,7 +845,7 @@ - (void)customModalPresentationControllerAnimation {
weakSelf.containerView.alpha = 0;
weakSelf.containerView.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1.0);
[UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
- weakSelf.maskView.alpha = 1;
+ weakSelf.dimmingView.alpha = 1;
weakSelf.containerView.alpha = 1;
weakSelf.containerView.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0);
} completion:^(BOOL finished) {
@@ -854,7 +856,7 @@ - (void)customModalPresentationControllerAnimation {
} else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) {
weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetMinY(weakSelf.containerView.frame), 0);
[UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
- weakSelf.maskView.alpha = 1;
+ weakSelf.dimmingView.alpha = 1;
weakSelf.containerView.layer.transform = CATransform3DIdentity;
} completion:^(BOOL finished) {
if (completion) {
@@ -867,7 +869,7 @@ - (void)customModalPresentationControllerAnimation {
self.modalPresentationViewController.hidingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)) {
if (self.preferredStyle == QMUIAlertControllerStyleAlert) {
[UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
- weakSelf.maskView.alpha = 0;
+ weakSelf.dimmingView.alpha = 0;
weakSelf.containerView.alpha = 0;
} completion:^(BOOL finished) {
weakSelf.containerView.alpha = 1;
@@ -877,7 +879,7 @@ - (void)customModalPresentationControllerAnimation {
}];
} else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) {
[UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
- weakSelf.maskView.alpha = 0;
+ weakSelf.dimmingView.alpha = 0;
weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetMinY(weakSelf.containerView.frame), 0);
} completion:^(BOOL finished) {
if (completion) {
@@ -896,11 +898,8 @@ - (void)showWithAnimated:(BOOL)animated {
if (self.alertTextFields.count > 0) {
[self.alertTextFields.firstObject becomeFirstResponder];
- } else {
- if (self.dismissKeyboardAutomatically && [QMUIKeyboardManager isKeyboardVisible]) {
- [[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];
- }
}
+
if (_needsUpdateAction) {
[self updateAction];
}
@@ -920,13 +919,13 @@ - (void)showWithAnimated:(BOOL)animated {
__weak __typeof(self)weakSelf = self;
[self.modalPresentationViewController showWithAnimated:animated completion:^(BOOL finished) {
- weakSelf.maskView.alpha = 1;
+ weakSelf.dimmingView.alpha = 1;
weakSelf.willShow = NO;
weakSelf.showing = YES;
- if (self.isNeedsHideAfterAlertShowed) {
- [self hideWithAnimated:self.isAnimatedForHideAfterAlertShowed];
- self.isNeedsHideAfterAlertShowed = NO;
- self.isAnimatedForHideAfterAlertShowed = NO;
+ if (weakSelf.isNeedsHideAfterAlertShowed) {
+ [weakSelf hideWithAnimated:weakSelf.isAnimatedForHideAfterAlertShowed];
+ weakSelf.isNeedsHideAfterAlertShowed = NO;
+ weakSelf.isAnimatedForHideAfterAlertShowed = NO;
}
if ([weakSelf.delegate respondsToSelector:@selector(didShowAlertController:)]) {
[weakSelf.delegate didShowAlertController:weakSelf];
@@ -964,7 +963,7 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(void))completion {
weakSelf.modalPresentationViewController = nil;
weakSelf.willShow = NO;
weakSelf.showing = NO;
- weakSelf.maskView.alpha = 0;
+ weakSelf.dimmingView.alpha = 0;
if (self.preferredStyle == QMUIAlertControllerStyleAlert) {
weakSelf.containerView.alpha = 0;
} else {
@@ -1028,6 +1027,7 @@ - (void)addTextFieldWithConfigurationHandler:(void (^)(QMUITextField *textField)
textField.textColor = self.alertTextFieldTextColor;
textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
+ textField.textInsets = self.alertTextFieldTextInsets;
textField.layer.borderColor = self.alertTextFieldBorderColor.CGColor;
textField.layer.borderWidth = PixelOne;
[self.headerScrollView addSubview:textField];
@@ -1053,7 +1053,7 @@ - (void)addCustomView:(UIView *)view {
- (void)setTitle:(NSString *)title {
_title = title;
if (!self.titleLabel) {
- self.titleLabel = [[UILabel alloc] init];
+ self.titleLabel = [[QMUILabel alloc] init];
self.titleLabel.numberOfLines = 0;
[self.headerScrollView addSubview:self.titleLabel];
}
@@ -1079,7 +1079,7 @@ - (void)updateTitleLabel {
- (void)setMessage:(NSString *)message {
_message = message;
if (!self.messageLabel) {
- self.messageLabel = [[UILabel alloc] init];
+ self.messageLabel = [[QMUILabel alloc] init];
self.messageLabel.numberOfLines = 0;
[self.headerScrollView addSubview:self.messageLabel];
}
@@ -1166,12 +1166,67 @@ - (void)updateAction {
return [self.alertTextFields copy];
}
-- (void)handleMaskViewEvent:(id)sender {
- if (_shouldRespondMaskViewTouch) {
+- (void)handleDimmingViewEvent:(id)sender {
+ if (_shouldRespondDimmingViewTouch) {
[self hideWithAnimated:YES completion:NULL];
}
}
+#pragma mark - Getters & Setters
+
+- (UIControl *)dimmingView {
+ if (!_dimmingView) {
+ _dimmingView = [[UIControl alloc] init];
+ _dimmingView.alpha = 0;
+ _dimmingView.backgroundColor = UIColorMask;
+ [_dimmingView addTarget:self action:@selector(handleDimmingViewEvent:) forControlEvents:UIControlEventTouchUpInside];
+ }
+ return _dimmingView;
+}
+
+- (UIView *)containerView {
+ if (!_containerView) {
+ _containerView = [[UIView alloc] init];
+ }
+ return _containerView;
+}
+
+- (UIView *)scrollWrapView {
+ if (!_scrollWrapView) {
+ _scrollWrapView = [[UIView alloc] init];
+ }
+ return _scrollWrapView;
+}
+
+- (UIScrollView *)headerScrollView {
+ if (!_headerScrollView) {
+ _headerScrollView = [[UIScrollView alloc] init];
+ _headerScrollView.scrollsToTop = NO;
+ _headerScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
+ [self updateHeaderBackgrondColor];
+ }
+ return _headerScrollView;
+}
+
+- (UIScrollView *)buttonScrollView {
+ if (!_buttonScrollView) {
+ _buttonScrollView = [[UIScrollView alloc] init];
+ _buttonScrollView.scrollsToTop = NO;
+ _buttonScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
+ }
+ return _buttonScrollView;
+}
+
+- (CALayer *)extendLayer {
+ if (!_extendLayer) {
+ _extendLayer = [CALayer layer];
+ _extendLayer.hidden = !self.isExtendBottomLayout;
+ [_extendLayer qmui_removeDefaultAnimations];
+ [self updateExtendLayerAppearance];
+ }
+ return _extendLayer;
+}
+
#pragma mark -
- (void)didClickAlertAction:(QMUIAlertAction *)alertAction {
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.h b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.h
new file mode 100644
index 00000000..46b88ad8
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.h
@@ -0,0 +1,94 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// QMUIAnimationHelper.h
+// WeRead
+//
+// Created by zhoonchen on 2018/9/3.
+//
+
+#import
+#import "QMUIEasings.h"
+
+@interface QMUIAnimationHelper : NSObject
+
+typedef NS_ENUM(NSInteger, QMUIAnimationEasings) {
+ QMUIAnimationEasingsLinear,
+ QMUIAnimationEasingsEaseInSine,
+ QMUIAnimationEasingsEaseOutSine,
+ QMUIAnimationEasingsEaseInOutSine,
+ QMUIAnimationEasingsEaseInQuad,
+ QMUIAnimationEasingsEaseOutQuad,
+ QMUIAnimationEasingsEaseInOutQuad,
+ QMUIAnimationEasingsEaseInCubic,
+ QMUIAnimationEasingsEaseOutCubic,
+ QMUIAnimationEasingsEaseInOutCubic,
+ QMUIAnimationEasingsEaseInQuart,
+ QMUIAnimationEasingsEaseOutQuart,
+ QMUIAnimationEasingsEaseInOutQuart,
+ QMUIAnimationEasingsEaseInQuint,
+ QMUIAnimationEasingsEaseOutQuint,
+ QMUIAnimationEasingsEaseInOutQuint,
+ QMUIAnimationEasingsEaseInExpo,
+ QMUIAnimationEasingsEaseOutExpo,
+ QMUIAnimationEasingsEaseInOutExpo,
+ QMUIAnimationEasingsEaseInCirc,
+ QMUIAnimationEasingsEaseOutCirc,
+ QMUIAnimationEasingsEaseInOutCirc,
+ QMUIAnimationEasingsEaseInBack,
+ QMUIAnimationEasingsEaseOutBack,
+ QMUIAnimationEasingsEaseInOutBack,
+ QMUIAnimationEasingsEaseInElastic,
+ QMUIAnimationEasingsEaseOutElastic,
+ QMUIAnimationEasingsEaseInOutElastic,
+ QMUIAnimationEasingsEaseInBounce,
+ QMUIAnimationEasingsEaseOutBounce,
+ QMUIAnimationEasingsEaseInOutBounce,
+ QMUIAnimationEasingsSpring, // 自定义任意弹簧曲线
+ QMUIAnimationEasingsSpringKeyboard // 系统键盘动画曲线
+};
+
+/**
+ * 动画插值器
+ * 根据给定的 easing 曲线,计算出初始值和结束值在当前的时间 time 对应的值。value 目前现在支持 NSNumber、UIColor 以及 NSValue 类型的 CGPoint、CGSize、CGRect、CGAffineTransform、UIEdgeInsets
+ * @param fromValue 初始值
+ * @param toValue 结束值
+ * @param time 当前帧时间
+ * @param easing 曲线,见`QMUIAnimationEasings`
+ */
++ (id)interpolateFromValue:(id)fromValue
+ toValue:(id)toValue
+ time:(CGFloat)time
+ easing:(QMUIAnimationEasings)easing;
+/**
+ * 动画插值器,支持弹簧参数
+ * mass|damping|stiffness|initialVelocity 仅在 QMUIAnimationEasingsSpring 的时候才生效
+ */
++ (id)interpolateSpringFromValue:(id)fromValue
+ toValue:(id)toValue
+ time:(CGFloat)time
+ mass:(CGFloat)mass
+ damping:(CGFloat)damping
+ stiffness:(CGFloat)stiffness
+ initialVelocity:(CGFloat)initialVelocity
+ easing:(QMUIAnimationEasings)easing;
+
+/**
+ 类似系统 UIScrollView 在拖拽到内容尽头时会越拖越难拖的效果。
+ @param fromValue 初始值,一般为 0。
+ @param toValue 目标值,也即你希望拖拽到的极限距离。
+ @param time 当前拖拽距离相对于极限距离的百分比,0 表示在 fromValue,1 表示拖拽到与极限距离相同的大小,大于1表示拖拽得比极限距离还远。
+ @param coeff 取值范围-1~+∞。值越大,拖拽的初期越容易拖动。例如 0.1 表示从头到尾都很难拖动,9表示一开始稍微拖一下就可以动很长距离(也可以理解为只需要很短的拖拽动作就可以很快接近极限距离)。-1 表示用默认的 0.55,也即系统的 UIScrollView 的系数。
+ @return 返回当前 time 对应的移动距离,返回值大于等于 fromValue,小于 toValue(只会无限接近,不可能等于)。
+ */
++ (CGFloat)bounceFromValue:(CGFloat)fromValue
+ toValue:(CGFloat)toValue
+ time:(CGFloat)time
+ coeff:(CGFloat)coeff;
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.m b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.m
new file mode 100644
index 00000000..a1fd0e31
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.m
@@ -0,0 +1,239 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// QMUIAnimationHelper.m
+// WeRead
+//
+// Created by zhoonchen on 2018/9/3.
+//
+
+#import "QMUIAnimationHelper.h"
+#import "QMUICore.h"
+
+#define SpringDefaultMass 1.0
+#define SpringDefaultDamping 18.0
+#define SpringDefaultStiffness 82.0
+#define SpringDefaultInitialVelocity 0.0
+
+@implementation QMUIAnimationHelper
+
++ (CGFloat)bounceFromValue:(CGFloat)fromValue toValue:(CGFloat)toValue time:(CGFloat)time coeff:(CGFloat)coeff {
+ // 以下算法来源于社区:
+ // How UIScrollView works: https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060
+ // Grant Paul's Twitter: https://twitter.com/chpwn/status/285540192096497664
+ coeff = coeff == -1 ? 0.55 : coeff;// 0.55 为系统 UIScrollView 的默认系数,这里我们也将其作为我们的默认系数
+ CGFloat d = toValue;
+ CGFloat x = (d - fromValue) * time;
+ CGFloat result = fromValue + d + 1.0 - (1.0 / (coeff * x / d + 1)) * d;
+// NSLog(@"[%.2f-%.2f], coeff = %.2f, x = %.2f, result = %.2f", fromValue, d, coeff, x, result);
+ return result;
+}
+
++ (id)interpolateFromValue:(id)fromValue
+ toValue:(id)toValue
+ time:(CGFloat)time
+ easing:(QMUIAnimationEasings)easing {
+ return [self interpolateSpringFromValue:fromValue toValue:toValue time:time mass:SpringDefaultMass damping:SpringDefaultDamping stiffness:SpringDefaultStiffness initialVelocity:SpringDefaultInitialVelocity easing:easing];
+}
+
+/*
+ * 插值器,遇到新的类型再添加
+ */
++ (id)interpolateSpringFromValue:(id)fromValue
+ toValue:(id)toValue
+ time:(CGFloat)time
+ mass:(CGFloat)mass
+ damping:(CGFloat)damping
+ stiffness:(CGFloat)stiffness
+ initialVelocity:(CGFloat)initialVelocity
+ easing:(QMUIAnimationEasings)easing {
+
+ if ([fromValue isKindOfClass:[NSNumber class]]) { // NSNumber
+ CGFloat from = [fromValue floatValue];
+ CGFloat to = [toValue floatValue];
+ CGFloat result = interpolateSpring(from, to, time, easing, mass, damping, stiffness, initialVelocity);
+ return [NSNumber numberWithFloat:result];
+ }
+
+ else if ([fromValue isKindOfClass:[UIColor class]]) { // UIColor
+ UIColor *from = (UIColor *)fromValue;
+ UIColor *to = (UIColor *)toValue;
+ CGFloat fromRed, toRed, curRed = 0;
+ CGFloat fromGreen, toGreen, curGreen = 0;
+ CGFloat fromBlue, toBlue, curBlue = 0;
+ CGFloat fromAlpha, toAlpha, curAlpha = 0;
+ [from getRed:&fromRed green:&fromGreen blue:&fromBlue alpha:&fromAlpha];
+ [to getRed:&toRed green:&toGreen blue:&toBlue alpha:&toAlpha];
+ curRed = interpolateSpring(fromRed, toRed, time, easing, mass, damping, stiffness, initialVelocity);
+ curGreen = interpolateSpring(fromGreen, toGreen, time, easing, mass, damping, stiffness, initialVelocity);
+ curBlue = interpolateSpring(fromBlue, toBlue, time, easing, mass, damping, stiffness, initialVelocity);
+ curAlpha = interpolateSpring(fromAlpha, toAlpha, time, easing, mass, damping, stiffness, initialVelocity);
+ UIColor *result = [UIColor colorWithRed:curRed green:curGreen blue:curBlue alpha:curAlpha];
+ return result;
+ }
+
+ else if ([fromValue isKindOfClass:[NSValue class]]) { // NSValue
+ const char *type = [(NSValue *)fromValue objCType];
+ if (strcmp(type, @encode(CGPoint)) == 0) {
+ CGPoint from = [fromValue CGPointValue];
+ CGPoint to = [toValue CGPointValue];
+ CGPoint result = CGPointMake(interpolateSpring(from.x, to.x, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.y, to.y, time, easing, mass, damping, stiffness, initialVelocity));
+ return [NSValue valueWithCGPoint:result];
+ }
+ else if (strcmp(type, @encode(CGSize)) == 0) {
+ CGSize from = [fromValue CGSizeValue];
+ CGSize to = [toValue CGSizeValue];
+ CGSize result = CGSizeMake(interpolateSpring(from.width, to.width, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.height, to.height, time, easing, mass, damping, stiffness, initialVelocity));
+ return [NSValue valueWithCGSize:result];
+ }
+ else if (strcmp(type, @encode(CGRect)) == 0) {
+ CGRect from = [fromValue CGRectValue];
+ CGRect to = [toValue CGRectValue];
+ CGRect result = CGRectMake(interpolateSpring(from.origin.x, to.origin.x, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.origin.y, to.origin.y, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.size.width, to.size.width, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.size.height, to.size.height, time, easing, mass, damping, stiffness, initialVelocity));
+ return [NSValue valueWithCGRect:result];
+ }
+ else if (strcmp(type, @encode(CGAffineTransform)) == 0) {
+ CGAffineTransform from = [fromValue CGAffineTransformValue];
+ CGAffineTransform to = [toValue CGAffineTransformValue];
+ CGAffineTransform result = CGAffineTransformIdentity;
+ result.a = interpolateSpring(from.a, to.a, time, easing, mass, damping, stiffness, initialVelocity);
+ result.b = interpolateSpring(from.b, to.b, time, easing, mass, damping, stiffness, initialVelocity);
+ result.c = interpolateSpring(from.c, to.c, time, easing, mass, damping, stiffness, initialVelocity);
+ result.d = interpolateSpring(from.d, to.d, time, easing, mass, damping, stiffness, initialVelocity);
+ result.tx = interpolateSpring(from.tx, to.tx, time, easing, mass, damping, stiffness, initialVelocity);
+ result.ty = interpolateSpring(from.ty, to.ty, time, easing, mass, damping, stiffness, initialVelocity);
+ return [NSValue valueWithCGAffineTransform:result];
+ }
+ else if (strcmp(type, @encode(UIEdgeInsets)) == 0) {
+ UIEdgeInsets from = [fromValue UIEdgeInsetsValue];
+ UIEdgeInsets to = [toValue UIEdgeInsetsValue];
+ UIEdgeInsets result = UIEdgeInsetsZero;
+ result.top = interpolateSpring(from.top, to.top, time, easing, mass, damping, stiffness, initialVelocity);
+ result.left = interpolateSpring(from.left, to.left, time, easing, mass, damping, stiffness, initialVelocity);
+ result.bottom = interpolateSpring(from.bottom, to.bottom, time, easing, mass, damping, stiffness, initialVelocity);
+ result.right = interpolateSpring(from.right, to.right, time, easing, mass, damping, stiffness, initialVelocity);
+ return [NSValue valueWithUIEdgeInsets:result];
+ }
+ }
+
+ return (time < 0.5) ? fromValue: toValue;
+}
+
+CGFloat interpolate(CGFloat from, CGFloat to, CGFloat time, QMUIAnimationEasings easing) {
+ return interpolateSpring(from, to, time, easing, SpringDefaultMass, SpringDefaultDamping, SpringDefaultStiffness, SpringDefaultInitialVelocity);
+}
+
+CGFloat interpolateSpring(CGFloat from, CGFloat to, CGFloat time, QMUIAnimationEasings easing, CGFloat springMass, CGFloat springDamping, CGFloat springStiffness, CGFloat springInitialVelocity) {
+ switch (easing) {
+ case QMUIAnimationEasingsLinear:
+ time = QMUI_Linear(time);
+ break;
+ case QMUIAnimationEasingsEaseInSine:
+ time = QMUI_EaseInSine(time);
+ break;
+ case QMUIAnimationEasingsEaseOutSine:
+ time = QMUI_EaseOutSine(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutSine:
+ time = QMUI_EaseInOutSine(time);
+ break;
+ case QMUIAnimationEasingsEaseInQuad:
+ time = QMUI_EaseInQuad(time);
+ break;
+ case QMUIAnimationEasingsEaseOutQuad:
+ time = QMUI_EaseOutQuad(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutQuad:
+ time = QMUI_EaseInOutQuad(time);
+ break;
+ case QMUIAnimationEasingsEaseInCubic:
+ time = QMUI_EaseInCubic(time);
+ break;
+ case QMUIAnimationEasingsEaseOutCubic:
+ time = QMUI_EaseOutCubic(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutCubic:
+ time = QMUI_EaseInOutCubic(time);
+ break;
+ case QMUIAnimationEasingsEaseInQuart:
+ time = QMUI_EaseInQuart(time);
+ break;
+ case QMUIAnimationEasingsEaseOutQuart:
+ time = QMUI_EaseOutQuart(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutQuart:
+ time = QMUI_EaseInOutQuart(time);
+ break;
+ case QMUIAnimationEasingsEaseInQuint:
+ time = QMUI_EaseInQuint(time);
+ break;
+ case QMUIAnimationEasingsEaseOutQuint:
+ time = QMUI_EaseOutQuint(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutQuint:
+ time = QMUI_EaseInOutQuint(time);
+ break;
+ case QMUIAnimationEasingsEaseInExpo:
+ time = QMUI_EaseInExpo(time);
+ break;
+ case QMUIAnimationEasingsEaseOutExpo:
+ time = QMUI_EaseOutExpo(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutExpo:
+ time = QMUI_EaseInOutExpo(time);
+ break;
+ case QMUIAnimationEasingsEaseInCirc:
+ time = QMUI_EaseInCirc(time);
+ break;
+ case QMUIAnimationEasingsEaseOutCirc:
+ time = QMUI_EaseOutCirc(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutCirc:
+ time = QMUI_EaseInOutCirc(time);
+ break;
+ case QMUIAnimationEasingsEaseInBack:
+ time = QMUI_EaseInBack(time);
+ break;
+ case QMUIAnimationEasingsEaseOutBack:
+ time = QMUI_EaseOutBack(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutBack:
+ time = QMUI_EaseInOutBack(time);
+ break;
+ case QMUIAnimationEasingsEaseInElastic:
+ time = QMUI_EaseInElastic(time);
+ break;
+ case QMUIAnimationEasingsEaseOutElastic:
+ time = QMUI_EaseOutElastic(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutElastic:
+ time = QMUI_EaseInOutElastic(time);
+ break;
+ case QMUIAnimationEasingsEaseInBounce:
+ time = QMUI_EaseInBounce(time);
+ break;
+ case QMUIAnimationEasingsEaseOutBounce:
+ time = QMUI_EaseOutBounce(time);
+ break;
+ case QMUIAnimationEasingsEaseInOutBounce:
+ time = QMUI_EaseInOutBounce(time);
+ break;
+ case QMUIAnimationEasingsSpring:
+ time = QMUI_EaseSpring(time, springMass, springDamping, springStiffness, springInitialVelocity);
+ break;
+ case QMUIAnimationEasingsSpringKeyboard:
+ time = QMUI_EaseSpring(time, SpringDefaultMass, SpringDefaultDamping, SpringDefaultStiffness, SpringDefaultInitialVelocity);
+ break;
+ default:
+ time = QMUI_Linear(time);
+ break;
+ }
+ return (to - from) * time + from;
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.h b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.h
new file mode 100644
index 00000000..6addd7c2
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.h
@@ -0,0 +1,140 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// QMUIDisplayLinkAnimation.h
+// WeRead
+//
+// Created by zhoonchen on 2018/9/3.
+//
+
+#import
+#import "QMUIAnimationHelper.h"
+
+#define SpringAnimationDefaultDuration 0.5
+
+
+/*
+ * 通过 CADisplayLink 来做动画,接口尽可能模拟 CAAnimation。有如下好处:
+ * 1、跟随系统刷新频率
+ * 2、因为使用了 CADisplayLink,所以理论上所有数据都可以做动画,而不局限于 CALayer 的 UI 属性
+ * 3、避免 CAAnimation 有时候系统会自动暂停(例如 app 退到后台再进来,或者切到其他界面再回来)
+ * 4、更多动画曲线可以选择,包括弹簧动画以及类似系统的键盘曲线动画。
+ * @warning: ⚠️⚠️⚠️ 当动画是无限循环的时候,需要在某个时机去 stop 动画(例如 dealloc 里面),否则 `QMUIDisplayLinkAnimation` 对象永远都不会释放,对应的 CADisplayLink 在后台都会被调用。
+ */
+
+@interface QMUIDisplayLinkAnimation : NSObject
+
+@property(nonatomic, strong, readonly) CADisplayLink *displayLink;
+
+@property(nonatomic, strong) id fromValue;
+@property(nonatomic, strong) id toValue;
+
+/// 动画时间
+@property(nonatomic, assign) NSTimeInterval duration;
+
+/// 动画曲线
+@property(nonatomic, assign) QMUIAnimationEasings easing;
+
+/// 是否需要重复,如果设置为YES,那么会无限重复动画,默认NO
+/// TODO: 目前功能上不支持小数点的循环次数,例如 0.5 1.5
+@property(nonatomic, assign) BOOL repeat;
+
+/// 延迟开始动画
+@property(nonatomic, assign) NSTimeInterval beginTime;
+
+/// 只有设置了repeat之后这个值才有用
+@property(nonatomic, assign) float repeatCount;
+
+/// 只有设置了repeat之后这个值才有用。如果YES,则往前做动画之后自动往后做动画,默认NO
+@property(nonatomic, assign) BOOL autoreverses;
+
+/// 做动画的block,适用于只有一个属性需要做动画,curValue是经过计算后当前帧的值
+@property(nonatomic, copy) void (^animation)(id curValue);
+
+/// 做动画的block,适用于多个属性做动画,需要在block里面自己计算当前帧的所有属性的值
+@property(nonatomic, copy) void (^animations)(QMUIDisplayLinkAnimation *animation, CGFloat curTime);
+
+- (instancetype)initWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ fromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation;
+
+- (instancetype)initWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations;
+
+/// 开始动画,无论是第一次做动画或者暂停之后再重新做动画,都调用这个方法
+- (void)startAnimation;
+
+/// 停止动画,CADisplayLink 对象会被移出
+- (void)stopAnimation;
+
+/// 即将开始做动画
+@property(nonatomic, copy) void (^willStartAnimation)(void);
+
+/// 动画结束
+@property(nonatomic, copy) void (^didStopAnimation)(void);
+
+@end
+
+
+@interface QMUIDisplayLinkAnimation (ConvenienceClassMethod)
+
+/*
+ * 这些类方法在动画执行之后会自动销毁 QMUIDisplayLinkAnimation 对象,因为此时没有人持有这个对象(有个坑就是如果这个动画是无限循环的,那么就一直无法销毁,需要业务手动销毁)。如果想要持有对象以便在后续操作,可以把返回值保存到其他属性里面。
+ * `createdBlock` 是 animation 创建之后,开始动画之前的回调,一般用来设置 animation 属性,比如是否重复动画以及重复的次数。
+ * `didStopBlock` 是动画结束之后的回调。
+ * @warning: block 中的代码记得使用弱引用,以免内存泄漏。
+ */
+
++ (instancetype)springAnimateWithFromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock;
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ fromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation;
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ fromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock;
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ fromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock
+ didStopBlock:(void (^)(QMUIDisplayLinkAnimation *animation))didStopBlock;
+
++ (instancetype)springAnimateWithAnimations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock;
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations;
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock;
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock
+ didStopBlock:(void (^)(QMUIDisplayLinkAnimation *animation))didStopBlock;
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.m b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.m
new file mode 100644
index 00000000..bc3c42b6
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.m
@@ -0,0 +1,290 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// QMUIDisplayLinkAnimation.m
+// WeRead
+//
+// Created by zhoonchen on 2018/9/3.
+//
+
+#import "QMUIDisplayLinkAnimation.h"
+#import "QMUICore.h"
+
+@interface QMUIDisplayLinkAnimation ()
+
+@property(nonatomic, strong, readwrite) CADisplayLink *displayLink;
+
+@property(nonatomic, assign) NSTimeInterval timeOffset;
+@property(nonatomic, assign) NSInteger curRepeatCount;
+@property(nonatomic, assign) BOOL isReversing;
+
+@end
+
+@implementation QMUIDisplayLinkAnimation
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
+ self.fromValue = nil;
+ self.toValue = nil;
+ self.duration = 0;
+ self.repeatCount = 0;
+ self.easing = QMUIAnimationEasingsLinear;
+ self.timeOffset = 0;
+ self.animation = nil;
+ }
+ return self;
+}
+
+- (instancetype)initWithDuration:(CFTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ fromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation {
+ if (self = [self init]) {
+ self.duration = duration;
+ self.easing = easing;
+ self.fromValue = fromValue;
+ self.toValue = toValue;
+ self.animation = animation;
+ }
+ return self;
+}
+
+- (instancetype)initWithDuration:(CFTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations {
+ if (self = [self init]) {
+ self.duration = duration;
+ self.easing = easing;
+ self.animations = animations;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [_displayLink invalidate];
+ _displayLink = nil;
+}
+
+- (void)startAnimation {
+ if (!self.displayLink) {
+ return;
+ }
+ if (self.displayLink.paused) {
+ self.displayLink.paused = NO;
+ return;
+ }
+ if (self.beginTime > 0) {
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.beginTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+ if (self.displayLink) {
+ if (self.willStartAnimation) {
+ self.willStartAnimation();
+ }
+ [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
+ }
+ });
+ } else {
+ if (self.willStartAnimation) {
+ self.willStartAnimation();
+ }
+ [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
+ }
+}
+
+- (void)stopAnimation {
+ [self.displayLink invalidate];
+ self.displayLink = nil;
+ if (self.didStopAnimation) {
+ self.didStopAnimation();
+ }
+}
+
+- (void)handleDisplayLink:(CADisplayLink *)displayLink {
+ if (!self.animation && !self.animations) {
+ return;
+ }
+ NSTimeInterval oneFrame = 1.0 / [self preferredFramesPerSecond];
+ if (self.autoreverses && self.isReversing) {
+ self.timeOffset = MAX(self.timeOffset - oneFrame, 0);
+ } else {
+ self.timeOffset = MIN(self.timeOffset + oneFrame, self.duration);
+ }
+ CGFloat time = self.timeOffset / self.duration;
+ if (self.animations) {
+ self.animations(self, time);
+ } else if (self.animation) {
+ id curValue = [QMUIAnimationHelper interpolateFromValue:self.fromValue toValue:self.toValue time:time easing:self.easing];
+ self.animation(curValue);
+ }
+ if (self.timeOffset >= self.duration) {
+ [self beginToDecrease];
+ } else if (self.timeOffset <= 0) {
+ [self beginToIncrease];
+ }
+}
+
+- (void)beginToIncrease {
+ if (self.repeat && self.repeatCount > 0) {
+ self.curRepeatCount++;
+ }
+ if (self.autoreverses) {
+ self.isReversing = NO;
+ }
+ if (self.curRepeatCount >= self.repeatCount) {
+ [self stopAnimation];
+ }
+}
+
+- (void)beginToDecrease {
+ if (self.repeat && self.repeatCount > 0) {
+ self.curRepeatCount++;
+ }
+ if (self.repeat) {
+ if (self.autoreverses) {
+ self.isReversing = YES;
+ } else {
+ self.timeOffset = 0;
+ }
+ if (self.curRepeatCount >= self.repeatCount) {
+ [self stopAnimation];
+ }
+ } else {
+ [self stopAnimation];
+ }
+}
+
+- (NSInteger)preferredFramesPerSecond {
+ if (self.displayLink.preferredFramesPerSecond == 0) {
+ // 不能写死60,而要拿当前设备支持的最大帧率来计算。根据 CADisplayLink 的官方文档,如果返回一个超过当前设备实际帧率的数字,实际依然会用设备实际帧率来计算,所以不用担心设备降频导致帧率降低后动画时长是否有问题。
+ return UIScreen.mainScreen.maximumFramesPerSecond;
+ }
+ return self.displayLink.preferredFramesPerSecond;
+}
+
+@end
+
+
+@implementation QMUIDisplayLinkAnimation (ConvenienceClassMethod)
+
++ (instancetype)springAnimateWithFromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock {
+ return [self animateWithDuration:SpringAnimationDefaultDuration
+ easing:QMUIAnimationEasingsSpringKeyboard
+ fromValue:fromValue
+ toValue:toValue
+ animation:animation
+ createdBlock:createdBlock];
+}
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ fromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation {
+ return [self animateWithDuration:duration
+ easing:easing
+ fromValue:fromValue
+ toValue:toValue
+ animation:animation
+ createdBlock:nil];
+}
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ fromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock {
+ return [self animateWithDuration:duration
+ easing:easing
+ fromValue:fromValue
+ toValue:toValue
+ animation:animation
+ createdBlock:createdBlock
+ didStopBlock:nil];
+}
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ fromValue:(id)fromValue
+ toValue:(id)toValue
+ animation:(void (^)(id curValue))animation
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock
+ didStopBlock:(void (^)(QMUIDisplayLinkAnimation *animation))didStopBlock {
+ QMUIDisplayLinkAnimation *displayLinkAnimation = [[self alloc] initWithDuration:duration
+ easing:easing
+ fromValue:fromValue
+ toValue:toValue
+ animation:animation];
+ if (createdBlock) {
+ createdBlock(displayLinkAnimation);
+ }
+ __weak QMUIDisplayLinkAnimation *weakDisplayLinkAnimation = displayLinkAnimation;
+ displayLinkAnimation.didStopAnimation = ^{
+ if (didStopBlock) {
+ didStopBlock(weakDisplayLinkAnimation);
+ }
+ };
+ [displayLinkAnimation startAnimation];
+ return displayLinkAnimation;
+}
+
++ (instancetype)springAnimateWithAnimations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock {
+ return [self animateWithDuration:SpringAnimationDefaultDuration
+ easing:QMUIAnimationEasingsSpringKeyboard
+ animations:animations
+ createdBlock:createdBlock];
+}
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations {
+ return [self animateWithDuration:duration
+ easing:easing
+ animations:animations
+ createdBlock:nil];
+}
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock {
+ return [self animateWithDuration:duration
+ easing:easing
+ animations:animations
+ createdBlock:createdBlock
+ didStopBlock:nil];
+}
+
++ (instancetype)animateWithDuration:(NSTimeInterval)duration
+ easing:(QMUIAnimationEasings)easing
+ animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations
+ createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock
+ didStopBlock:(void (^)(QMUIDisplayLinkAnimation *animation))didStopBlock {
+ QMUIDisplayLinkAnimation *displayLinkAnimation = [[self alloc] initWithDuration:duration
+ easing:easing
+ animations:animations];
+ if (createdBlock) {
+ createdBlock(displayLinkAnimation);
+ }
+ __weak QMUIDisplayLinkAnimation *weakDisplayLinkAnimation = displayLinkAnimation;
+ displayLinkAnimation.didStopAnimation = ^{
+ if (didStopBlock) {
+ didStopBlock(weakDisplayLinkAnimation);
+ }
+ };
+ [displayLinkAnimation startAnimation];
+ return displayLinkAnimation;
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIEasings.h b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIEasings.h
new file mode 100644
index 00000000..938e40cf
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIEasings.h
@@ -0,0 +1,233 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// QMUIEasings.h
+// WeRead
+//
+// Created by zhoonchen on 2018/9/3.
+//
+
+#import
+
+/// https://easings.net
+/// http://cubic-bezier.com
+
+CG_INLINE CGFloat
+QMUI_Linear(CGFloat t) {
+ return t;
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInSine(CGFloat t) {
+ return 1 - cos(t * M_PI_2);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutSine(CGFloat t) {
+ return sin(t * M_PI_2);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutSine(CGFloat t) {
+ return - (cos(M_PI * t) - 1) / 2;
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInQuad(CGFloat t) {
+ return pow(t, 2);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutQuad(CGFloat t) {
+ return 1 - pow(1 - t, 2);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutQuad(CGFloat t) {
+ return t < 0.5 ? (2 * pow(t, 2)) : (1 - pow(-2 * t + 2, 2) / 2);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInCubic(CGFloat t) {
+ return pow(t, 3);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutCubic(CGFloat t) {
+ return 1 - pow(1 - t, 3);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutCubic(CGFloat t) {
+ return t < 0.5 ? (4 * pow(t, 3)) : (1 - pow(-2 * t + 2, 3) / 2);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInQuart(CGFloat t) {
+ return pow(t, 4);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutQuart(CGFloat t) {
+ return 1 - pow(1 - t, 4);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutQuart(CGFloat t) {
+ return t < 0.5 ? (8 * pow(t, 4)) : (1 - pow(-2 * t + 2, 4) / 2);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInQuint(CGFloat t) {
+ return pow(t, 5);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutQuint(CGFloat t) {
+ return 1 - pow(1 - t, 5);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutQuint(CGFloat t) {
+ return t < 0.5 ? (16 * pow(t, 5)) : (1 - pow(-2 * t + 2, 5) / 2);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInExpo(CGFloat t) {
+ return t == 0 ? 0 : pow(2, 10 * t - 10);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutExpo(CGFloat t) {
+ return t == 1 ? 1 : 1 - pow(2, -10 * t);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutExpo(CGFloat t) {
+ return t == 0 ? 0 : t == 1 ? 1 : t < 0.5 ? pow(2, 20 * t - 10 ) / 2 : (2 - pow(2, -20 * t + 10 )) / 2;
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInCirc(CGFloat t) {
+ return 1 - sqrt(1 - pow(t, 2));
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutCirc(CGFloat t) {
+ return sqrt(1 - pow(t - 1, 2));
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutCirc(CGFloat t) {
+ return t < 0.5 ? (1 - sqrt(1 - pow(2 * t, 2))) / 2 : (sqrt(1 - pow(-2 * t + 2, 2)) + 1) / 2;
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInBack(CGFloat t) {
+ return pow(t, 3) - t * sin(t * M_PI);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutBack(CGFloat t) {
+ CGFloat f = (1 - t);
+ return 1 - (pow(f, 3) - f * sin(f * M_PI));
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutBack(CGFloat t) {
+ if (t < 0.5) {
+ CGFloat f = 2 * t;
+ return 0.5 * (pow(f, 3) - f * sin(f * M_PI));
+ } else {
+ CGFloat f = (1 - (2 * t - 1));
+ return 0.5 * (1 - (pow(f, 3) - f * sin(f * M_PI))) + 0.5;
+ }
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInElastic(CGFloat t) {
+ return sin(13 * M_PI_2 * t) * pow(2, 10 * (t - 1));
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutElastic(CGFloat t) {
+ return sin(-13 * M_PI_2 * (t + 1)) * pow(2, -10 * t) + 1;
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutElastic(CGFloat t) {
+ if (t < 0.5) {
+ return 0.5 * sin(13 * M_PI_2 * (2 * t)) * pow(2, 10 * ((2 * t) - 1));
+ } else {
+ return 0.5 * (sin(-13 * M_PI_2 * ((2 * t - 1) + 1)) * pow(2, -10 * (2 * t - 1)) + 2);
+ }
+}
+
+CG_INLINE CGFloat
+QMUI_EaseOutBounce(CGFloat t) {
+ if (t < 4.0 / 11.0) {
+ return (121.0 * t * t) / 16.0;
+ } else if (t < 8.0 / 11.0) {
+ return (363.0 / 40.0 * t * t) - (99.0 / 10.0 * t) + 17.0 / 5.0;
+ } else if(t < 9.0 / 10.0) {
+ return (4356.0 / 361.0 * t * t) - (35442.0 / 1805.0 * t) + 16061.0 / 1805.0;
+ } else {
+ return (54.0 / 5.0 * t * t) - (513.0 / 25.0 * t) + 268.0 / 25.0;
+ }
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInBounce(CGFloat t) {
+ return 1 - QMUI_EaseOutBounce(1 - t);
+}
+
+CG_INLINE CGFloat
+QMUI_EaseInOutBounce(CGFloat t) {
+ if (t < 0.5) {
+ return 0.5 * QMUI_EaseInBounce(t * 2);
+ } else {
+ return 0.5 * QMUI_EaseOutBounce(t * 2 - 1) + 0.5;
+ }
+}
+
+CG_INLINE CGFloat
+QMUI_EaseSpring(CGFloat t, CGFloat mass, CGFloat damping, CGFloat stiffness, CGFloat initialVelocity) {
+
+ // https://webkit.org/demos/spring/spring.js
+ // https://webkit.org/demos/spring
+
+ CGFloat m_w0 = sqrt(stiffness / mass);
+ CGFloat m_zeta = damping / (2 * sqrt(stiffness * mass));
+
+ CGFloat m_wd = 0;
+ CGFloat m_A = 0;
+ CGFloat m_B = 0;
+
+ if (m_zeta < 1) {
+ // Under-damped.
+ m_wd = m_w0 * sqrt(1 - m_zeta * m_zeta);
+ m_A = 1;
+ m_B = (m_zeta * m_w0 + -initialVelocity) / m_wd;
+ } else {
+ // Critically damped (ignoring over-damped case for now).
+ m_wd = 0;
+ m_A = 1;
+ m_B = -initialVelocity + m_w0;
+ }
+
+ if (m_zeta < 1) {
+ // Under-damped
+ t = exp(-t * m_zeta * m_w0) * (m_A * cos(m_wd * t) + m_B * sin(m_wd * t));
+ } else {
+ // Critically damped (ignoring over-damped case for now).
+ t = (m_A + m_B * t) * exp(-t * m_w0);
+ }
+
+ // Map range from [1..0] to [0..1].
+ return 1 - t;
+}
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.h b/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.h
new file mode 100644
index 00000000..f4f96b09
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.h
@@ -0,0 +1,52 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// QMUIAppearance.h
+// QMUIKit
+//
+// Created by MoLice on 2020/3/25.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+UIKit 仅提供了对 UIView 默认的 UIAppearance 支持,如果你是一个继承自 NSObject 的对象,想要使用 UIAppearance 能力,按 UIKit 公开的 API 是无法实现的,而 QMUIAppearance 对这种场景提供了支持。
+
+使用方法(可参考 QMUIAlertController):
+
+1. 为目标类增加方法 +(instancetype)appearance; 方法,返回值类型使用 instancetype 是为了保证 Xcode 能正确进行代码提示,命名无限制,用 appearance 只是为了统一。
+
+2. 为目标类支持 appearance 的属性、方法添加 UI_APPEARANCE_SELECTOR 标记,注意对于方法只有符合特定命名格式才支持,具体请查看 UIAppearance.h 顶部对宏 UI_APPEARANCE_SELECTOR 的注释。
+
+3. 在 +appearance 方法里通过 +[QMUIAppearance appearanceForClass:self] 得到 appearance 对象并返回。
+
+4. 在恰当的时机为目标类的 appearance 赋初始值,QMUI 通常在类的 +initialize 方法里赋值。如果你支持 UI_APPEARANCE_SELECTOR 的属性默认值都为 nil,也可以忽略这一步。
+
+5. 在类初始化实例的时候(例如 init 方法里)调用 -qmui_applyQMUIAppearance 为实例赋初始值,注意如果你的父类已经调用过的话,子类不需要再重复调用。
+
+@note 特别的,如果你正在为一个 UIView 子类支持 UIAppearance,不需要用到 QMUIAppearance,直接将属性、方法加上 UI_APPEARANCE_SELECTOR 标记即可,也不需要通过 -qmui_applyAppearance 的方式赋初始值(除非你希望这个赋值时机提前,系统默认时机是在 didMoveToWindow 时),系统都已经帮你处理好了,具体可查看 UIKit Documentation。
+*/
+@interface QMUIAppearance : NSObject
+
+/**
+ 获取指定 Class 的 appearance 对象,每个 Class 全局只会存在一个 appearance 对象。
+ */
++ (id)appearanceForClass:(Class)aClass;
+@end
+
+@interface NSObject (QMUIAppearnace)
+
+/**
+ 从 appearance 里取值并赋值给当前实例,通常在对象的 init 里调用(只要在实例初始化后、使用前就可以)。适用于 QMUIAppearance 和系统的 UIAppearance。
+ */
+- (void)qmui_applyAppearance;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.m b/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.m
new file mode 100644
index 00000000..cb712939
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.m
@@ -0,0 +1,80 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+//
+// QMUIAppearance.m
+// QMUIKit
+//
+// Created by MoLice on 2020/3/25.
+//
+
+#import "QMUIAppearance.h"
+#import "QMUICore.h"
+#import "QMUIWeakObjectContainer.h"
+
+@implementation QMUIAppearance
+
+static NSMutableDictionary *appearances;
++ (id)appearanceForClass:(Class)aClass {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ if (!appearances) {
+ appearances = NSMutableDictionary.new;
+ }
+ });
+ NSString *className = NSStringFromClass(aClass);
+ id appearance = appearances[className];
+ if (!appearance) {
+ BeginIgnorePerformSelectorLeaksWarning
+ SEL selector = NSSelectorFromString([NSString stringWithFormat:@"_%@:%@:", @"appearanceForClass", @"withContainerList"]);
+ appearance = [NSClassFromString(@"_UIAppearance") performSelector:selector withObject:aClass withObject:nil];
+ appearances[className] = appearance;
+ EndIgnorePerformSelectorLeaksWarning
+ }
+ return appearance;
+}
+
+@end
+
+BeginIgnoreClangWarning(-Wincomplete-implementation)
+@interface NSObject (QMUIAppearance_Private)
+@property(nonatomic, assign) BOOL qmui_applyingAppearance;
++ (instancetype)appearance;
+@end
+
+@implementation NSObject (QMUIAppearnace)
+QMUISynthesizeBOOLProperty(qmui_applyingAppearance, setQmui_applyingAppearance)
+
+/**
+ 关于 appearance 要考虑这几点:
+ 1. 是否产生内存泄漏
+ 2. 父类的 appearance 能否在子类里生效
+ 3. 如果某个 property 在 ClassA 里声明为 UI_APPEARANCE_SELECTOR,则在子类 Class B : Class A 里获取该 property 的值将为 nil,这是正常的,系统默认行为如此,系统是在应用 appearance 的时候发现子类的 property 值为 nil 时才会从父类里读取值,在这个阶段才完成继承效果。
+ */
+- (void)qmui_applyAppearance {
+ Class class = self.class;
+ if ([class respondsToSelector:@selector(appearance)]) {
+ // -[_UIAppearance _applyInvocationsTo:window:] 会调用 _appearanceGuideClass,如果不是 UIView 或者 UIViewController 的子类,需要额外实现这个方法。
+ SEL appearanceGuideClassSelector = NSSelectorFromString(@"_appearanceGuideClass");
+ if (!class_respondsToSelector(class, appearanceGuideClassSelector)) {
+ const char * typeEncoding = method_getTypeEncoding(class_getInstanceMethod(UIView.class, appearanceGuideClassSelector));
+ class_addMethod(class, appearanceGuideClassSelector, imp_implementationWithBlock(^Class(void) {
+ return nil;
+ }), typeEncoding);
+ }
+
+ self.qmui_applyingAppearance = YES;
+ BeginIgnorePerformSelectorLeaksWarning
+ SEL selector = NSSelectorFromString([NSString stringWithFormat:@"_%@:%@:", @"applyInvocationsTo", @"window"]);
+ [NSClassFromString(@"_UIAppearance") performSelector:selector withObject:self withObject:nil];
+ EndIgnorePerformSelectorLeaksWarning
+ self.qmui_applyingAppearance = NO;
+ }
+}
+
+@end
+EndIgnoreClangWarning
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.h b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.h
new file mode 100644
index 00000000..7d589c3c
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.h
@@ -0,0 +1,17 @@
+//
+// QMUIBadgeLabel.h
+// QMUIKit
+//
+// Created by molice on 2023/7/26.
+// Copyright © 2023 QMUI Team. All rights reserved.
+//
+
+#import "QMUILabel.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface QMUIBadgeLabel : QMUILabel
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.m b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.m
new file mode 100644
index 00000000..27e1d516
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.m
@@ -0,0 +1,56 @@
+//
+// QMUIBadgeLabel.m
+// QMUIKit
+//
+// Created by molice on 2023/7/26.
+// Copyright © 2023 QMUI Team. All rights reserved.
+//
+
+#import "QMUIBadgeLabel.h"
+#import "QMUICore.h"
+
+@implementation QMUIBadgeLabel
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ self.clipsToBounds = YES;
+ self.textAlignment = NSTextAlignmentCenter;
+ if (@available(iOS 13.0, *)) {
+ self.layer.cornerCurve = kCACornerCurveContinuous;
+ }
+
+ if (QMUICMIActivated) {
+ self.backgroundColor = BadgeBackgroundColor;
+ self.textColor = BadgeTextColor;
+ self.font = BadgeFont;
+ self.contentEdgeInsets = BadgeContentEdgeInsets;
+ } else {
+ self.backgroundColor = UIColorRed;
+ self.textColor = UIColorWhite;
+ self.font = UIFontBoldMake(11);
+ self.contentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4);
+ }
+ }
+ return self;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+ if (self.attributedText.length == 1) {
+ NSMutableAttributedString *text = self.attributedText.mutableCopy;
+ [text replaceCharactersInRange:NSMakeRange(0, 1) withString:@"8"];
+ CGSize textSize = [text boundingRectWithSize:CGSizeMax options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
+ CGSize result = CGSizeFlatted(CGSizeMake(textSize.width + UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), textSize.height + UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets)));
+ result.width = MAX(result.width, result.height);
+ result.height = result.width;
+ return result;
+ }
+ CGSize result = [super sizeThatFits:size];
+ return result;
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ self.layer.cornerRadius = MIN(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2);
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h
new file mode 100644
index 00000000..ee673fff
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h
@@ -0,0 +1,83 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// QMUIBadgeProtocol.h
+// QMUIKit
+//
+// Created by MoLice on 2020/5/26.
+//
+
+#import
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol QMUIBadgeProtocol
+
+#pragma mark - Badge
+
+/// 用数字设置未读数,0表示不显示未读数。
+/// @note 仅当 qmui_badgeView 为 UILabel 及其子类时才会自动设置到 qmui_badgeView 上。
+@property(nonatomic, assign) NSUInteger qmui_badgeInteger;
+
+/// 用字符串设置未读数,nil 表示不显示未读数
+/// @note 仅当 qmui_badgeView 为 UILabel 及其子类时才会自动设置到 qmui_badgeView 上。
+@property(nonatomic, copy, nullable) NSString *qmui_badgeString;
+
+@property(nonatomic, strong, nullable) UIColor *qmui_badgeBackgroundColor;
+
+/// 未读数的文字颜色
+/// @note 仅当 qmui_badgeView 为 UILabel 及其子类时才会自动设置到 qmui_badgeView 上。
+@property(nonatomic, strong, nullable) UIColor *qmui_badgeTextColor;
+
+/// 未读数的字体
+/// @note 仅当 qmui_badgeView 为 UILabel 及其子类时才会自动设置到 qmui_badgeView 上。
+@property(nonatomic, strong, nullable) UIFont *qmui_badgeFont;
+
+/// 未读数字与圆圈之间的 padding,会影响最终 badge 的大小。当只有一位数字时,会取宽/高中最大的值作为最终的宽高,以保证整个 badge 是正圆。
+/// /// @note 仅当 qmui_badgeView 为 QMUILabel 及其子类时才会自动设置到 qmui_badgeView 上。
+@property(nonatomic, assign) UIEdgeInsets qmui_badgeContentEdgeInsets;
+
+/// 默认 badge 的布局处于 view 右上角(x = view.width, y = -badge height),通过这个属性可以调整 badge 相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。
+/// 特别地,对于普通的 UITabBarItem 和 UIBarButtonItem,badge 布局相对于内部的 imageView 而不是按钮本身,如果该 item 使用了 customView 则相对于按钮本身。
+@property(nonatomic, assign) CGPoint qmui_badgeOffset;
+
+/// 横屏下使用,其他同 @c qmui_badgeOffset 。
+@property(nonatomic, assign) CGPoint qmui_badgeOffsetLandscape;
+
+/// 未读数的 view,默认是 QMUIBadgeLabel,也可设置为自定义的 view。自定义 view 如果是 UILabel 类型则内部会自动为其设置 text、textColor,但如果是其他类型的 view 则需要业务自行处理。
+@property(nonatomic, strong, nullable) __kindof UIView *qmui_badgeView;
+
+/// badgeView 布局完成后的回调。因为 badgeView 必定在当前 view 的 layoutSubviews 执行完之后才布局,所以业务很难在自己的 layoutSubviews 里重新调整 badgeView 的布局,所以提供一个 block。
+@property(nonatomic, copy, nullable) void (^qmui_badgeViewDidLayoutBlock)(__kindof UIView *aView, __kindof UIView *aBadgeView);
+
+
+#pragma mark - UpdatesIndicator
+
+/// 控制红点的显隐
+@property(nonatomic, assign) BOOL qmui_shouldShowUpdatesIndicator;
+@property(nonatomic, strong, nullable) UIColor *qmui_updatesIndicatorColor;
+@property(nonatomic, assign) CGSize qmui_updatesIndicatorSize;
+
+/// 默认红点的布局处于 view 右上角(x = view.width, y = -badge height),通过这个属性可以调整红点相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。
+/// 特别地,对于普通的 UITabBarItem 和 UIBarButtonItem,红点相对于内部的 imageView 布局而不是按钮本身,如果该 item 使用了 customView 则相对于按钮本身。
+@property(nonatomic, assign) CGPoint qmui_updatesIndicatorOffset;
+
+/// 横屏下使用,其他同 @c qmui_updatesIndicatorOffset 。
+@property(nonatomic, assign) CGPoint qmui_updatesIndicatorOffsetLandscape;
+
+/// 未读红点的 view,支持设置为自定义 view。
+@property(nonatomic, strong, nullable) __kindof UIView *qmui_updatesIndicatorView;
+
+/// updatesIndicatorView 布局完成后的回调。因为 updatesIndicatorView 必定在当前 view 的 layoutSubviews 执行完之后才布局,所以业务很难在自己的 layoutSubviews 里重新调整 updatesIndicatorView 的布局,所以提供一个 block。
+@property(nonatomic, copy, nullable) void (^qmui_updatesIndicatorViewDidLayoutBlock)(__kindof UIView *aView, __kindof UIView *aUpdatesIndicatorView);
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h
index 7560f16c..a5e31442 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// UIBarItem+QMUIBadge.h
@@ -15,58 +15,14 @@
#import
#import
-
-@class QMUILabel;
+#import "QMUIBadgeProtocol.h"
/**
- * 用于在 UIBarButtonItem(通常用于 UINavigationBar 和 UIToolbar)和 UITabBarItem 上显示未读红点或者未读数。对设置的时机没有要求。所有属性在 QMUIConfigurationTemplate 配置表里均提供对应的默认值设置,如果你不使用配置表,则所有属性的默认值均为 0。
+ * 用于在 UIBarButtonItem(通常用于 UINavigationBar 和 UIToolbar)和 UITabBarItem 上显示未读红点或者未读数,对设置的时机没有要求。
+ * 提供的属性请查看 @c QMUIBadgeProtocol ,属性的默认值在 QMUIConfigurationTemplate 配置表里设置,如果不使用配置表,则所有属性的默认值均为 0 或 nil。
*
* @note 系统对 UIBarButtonItem 和 UITabBarItem 在横竖屏下均会有不同的布局,当你使用本控件时建议分别检查横竖屏下的表现是否正确。
*/
-@interface UIBarItem (QMUIBadge)
-
-
-#pragma mark - Badge
-
-/// 用数字设置未读数,0表示不显示未读数
-@property(nonatomic, assign) NSUInteger qmui_badgeInteger;
-
-/// 用字符串设置未读数,nil 表示不显示未读数
-@property(nonatomic, copy, nullable) NSString *qmui_badgeString;
-
-@property(nonatomic, strong, nullable) UIColor *qmui_badgeBackgroundColor;
-@property(nonatomic, strong, nullable) UIColor *qmui_badgeTextColor;
-@property(nonatomic, strong, nullable) UIFont *qmui_badgeFont;
-
-/// 未读数字与圆圈之间的 padding,会影响最终 badge 的大小。当只有一位数字时,会取宽/高中最大的值作为最终的宽高,以保证整个 badge 是正圆。
-@property(nonatomic, assign) UIEdgeInsets qmui_badgeContentEdgeInsets;
-
-/// 默认 badge 的布局处于 item 正中心,而通过这个属性可以调整 badge 相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。
-/// 特别地,对于 UITabBarItem,badge 布局默认处于内部的 imageView 的正中心(而不是 item 的正中心)。
-@property(nonatomic, assign) CGPoint qmui_badgeCenterOffset;
-
-/// 默认 badge 的布局处于 item 正中心,而通过这个属性可以调整横屏模式下 badge 相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。
-/// 特别地,对于 UITabBarItem,badge 布局默认处于内部的 imageView 的正中心(而不是 item 的正中心)。
-@property(nonatomic, assign) CGPoint qmui_badgeCenterOffsetLandscape;
-
-@property(nonatomic, strong, readonly, nullable) QMUILabel *qmui_badgeLabel;
-
-
-#pragma mark - UpdatesIndicator
-
-/// 控制红点的显隐
-@property(nonatomic, assign) BOOL qmui_shouldShowUpdatesIndicator;
-@property(nonatomic, strong, nullable) UIColor *qmui_updatesIndicatorColor;
-@property(nonatomic, assign) CGSize qmui_updatesIndicatorSize;
-
-/// 默认红点的布局处于 item 正中心,而通过这个属性可以调整红点相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。
-/// 特别地,对于 UITabBarItem,红点布局默认处于内部的 imageView 的正中心(而不是 item 的正中心)。
-@property(nonatomic, assign) CGPoint qmui_updatesIndicatorCenterOffset;
-
-/// 默认红点的布局处于 item 正中心,而通过这个属性可以调整横屏模式下红点相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。
-/// 特别地,对于 UITabBarItem,红点布局默认处于内部的 imageView 的正中心(而不是 item 的正中心)。
-@property(nonatomic, assign) CGPoint qmui_updatesIndicatorCenterOffsetLandscape;
-
-@property(nonatomic, strong, readonly, nullable) UIView *qmui_updatesIndicatorView;
+@interface UIBarItem (QMUIBadge)
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m
index 731f35d2..9dbb0490 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// UIBarItem+QMUIBadge.m
@@ -15,30 +15,8 @@
#import "UIBarItem+QMUIBadge.h"
#import "QMUICore.h"
-#import "QMUILog.h"
-#import "QMUILabel.h"
-#import "UIView+QMUI.h"
+#import "UIView+QMUIBadge.h"
#import "UIBarItem+QMUI.h"
-#import "UITabBarItem+QMUI.h"
-#import "UIViewController+QMUI.h"
-
-@interface _QMUIBadgeLabel : QMUILabel
-
-@property(nonatomic, assign) CGPoint centerOffset;
-@property(nonatomic, assign) CGPoint centerOffsetLandscape;
-@end
-
-@interface _QMUIUpdatesIndicatorView : UIView
-
-@property(nonatomic, assign) CGPoint centerOffset;
-@property(nonatomic, assign) CGPoint centerOffsetLandscape;
-@end
-
-@interface UIBarItem ()
-
-@property(nonatomic, strong, readwrite) _QMUIBadgeLabel *qmui_badgeLabel;
-@property(nonatomic, strong, readwrite) _QMUIUpdatesIndicatorView *qmui_updatesIndicatorView;
-@end
@implementation UIBarItem (QMUIBadge)
@@ -60,17 +38,10 @@ + (void)load {
OverrideImplementation(NSClassFromString([NSString stringWithFormat:@"%@%@", @"UITab", @"BarButton"]), @selector(addSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UIView *selfObject, UIView *firstArgv) {
- if ([selfObject isKindOfClass:originClass] && firstArgv.superview == selfObject) {
+ if (firstArgv.superview == selfObject) {
return;
}
- if (selfObject == firstArgv) {
- UIViewController *visibleViewController = [QMUIHelper visibleViewController];
- NSString *log = [NSString stringWithFormat:@"UIBarItem (QMUIBadge) addSubview:, 把自己作为 subview 添加到自己身上,self = %@, visibleViewController = %@, visibleState = %@, viewControllers = %@\n%@", selfObject, visibleViewController, @(visibleViewController.qmui_visibleState), visibleViewController.navigationController.viewControllers, [NSThread callStackSymbols]];
- NSAssert(NO, log);
- QMUILogWarn(@"UIBarItem (QMUIBadge)", @"%@", log);
- }
-
// call super
IMP originalIMP = originalIMPProvider();
void (*originSelectorIMP)(id, SEL, UIView *);
@@ -82,17 +53,19 @@ + (void)load {
}
- (void)qmuibaritem_didInitialize {
- self.qmui_badgeBackgroundColor = BadgeBackgroundColor;
- self.qmui_badgeTextColor = BadgeTextColor;
- self.qmui_badgeFont = BadgeFont;
- self.qmui_badgeContentEdgeInsets = BadgeContentEdgeInsets;
- self.qmui_badgeCenterOffset = BadgeCenterOffset;
- self.qmui_badgeCenterOffsetLandscape = BadgeCenterOffsetLandscape;
-
- self.qmui_updatesIndicatorColor = UpdatesIndicatorColor;
- self.qmui_updatesIndicatorSize = UpdatesIndicatorSize;
- self.qmui_updatesIndicatorCenterOffset = UpdatesIndicatorCenterOffset;
- self.qmui_updatesIndicatorCenterOffsetLandscape = UpdatesIndicatorCenterOffsetLandscape;
+ if (QMUICMIActivated) {
+ self.qmui_badgeBackgroundColor = BadgeBackgroundColor;
+ self.qmui_badgeTextColor = BadgeTextColor;
+ self.qmui_badgeFont = BadgeFont;
+ self.qmui_badgeContentEdgeInsets = BadgeContentEdgeInsets;
+ self.qmui_badgeOffset = BadgeOffset;
+ self.qmui_badgeOffsetLandscape = BadgeOffsetLandscape;
+
+ self.qmui_updatesIndicatorColor = UpdatesIndicatorColor;
+ self.qmui_updatesIndicatorSize = UpdatesIndicatorSize;
+ self.qmui_updatesIndicatorOffset = UpdatesIndicatorOffset;
+ self.qmui_updatesIndicatorOffsetLandscape = UpdatesIndicatorOffsetLandscape;
+ }
}
#pragma mark - Badge
@@ -111,40 +84,9 @@ - (NSUInteger)qmui_badgeInteger {
- (void)setQmui_badgeString:(NSString *)qmui_badgeString {
objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeString, qmui_badgeString, OBJC_ASSOCIATION_COPY_NONATOMIC);
if (qmui_badgeString.length) {
- if (!self.qmui_badgeLabel) {
- self.qmui_badgeLabel = [[_QMUIBadgeLabel alloc] init];
- self.qmui_badgeLabel.clipsToBounds = YES;
- self.qmui_badgeLabel.textAlignment = NSTextAlignmentCenter;
- self.qmui_badgeLabel.backgroundColor = self.qmui_badgeBackgroundColor;
- self.qmui_badgeLabel.textColor = self.qmui_badgeTextColor;
- self.qmui_badgeLabel.font = self.qmui_badgeFont;
- self.qmui_badgeLabel.contentEdgeInsets = self.qmui_badgeContentEdgeInsets;
- self.qmui_badgeLabel.centerOffset = self.qmui_badgeCenterOffset;
- self.qmui_badgeLabel.centerOffsetLandscape = self.qmui_badgeCenterOffsetLandscape;
- if (!self.qmui_viewDidSetBlock) {
- self.qmui_viewDidSetBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) {
- [view addSubview:item.qmui_updatesIndicatorView];
- [view addSubview:item.qmui_badgeLabel];
- [view setNeedsLayout];
- [view layoutIfNeeded];
- };
- }
- // 之前 item 已经 set 完 view,则手动触发一次
- if (self.qmui_view) {
- self.qmui_viewDidSetBlock(self, self.qmui_view);
- }
- if (!self.qmui_viewDidLayoutSubviewsBlock) {
- self.qmui_viewDidLayoutSubviewsBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) {
- [item layoutSubviews];
- };
- }
- }
- self.qmui_badgeLabel.text = qmui_badgeString;
- self.qmui_badgeLabel.hidden = NO;
- [self setNeedsUpdateBadgeLabelLayout];
- } else {
- self.qmui_badgeLabel.hidden = YES;
+ [self updateViewDidSetBlockIfNeeded];
}
+ self.qmui_view.qmui_badgeString = qmui_badgeString;
}
- (NSString *)qmui_badgeString {
@@ -154,9 +96,7 @@ - (NSString *)qmui_badgeString {
static char kAssociatedObjectKey_badgeBackgroundColor;
- (void)setQmui_badgeBackgroundColor:(UIColor *)qmui_badgeBackgroundColor {
objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeBackgroundColor, qmui_badgeBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_badgeLabel) {
- self.qmui_badgeLabel.backgroundColor = qmui_badgeBackgroundColor;
- }
+ self.qmui_view.qmui_badgeBackgroundColor = qmui_badgeBackgroundColor;
}
- (UIColor *)qmui_badgeBackgroundColor {
@@ -166,9 +106,7 @@ - (UIColor *)qmui_badgeBackgroundColor {
static char kAssociatedObjectKey_badgeTextColor;
- (void)setQmui_badgeTextColor:(UIColor *)qmui_badgeTextColor {
objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeTextColor, qmui_badgeTextColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_badgeLabel) {
- self.qmui_badgeLabel.textColor = qmui_badgeTextColor;
- }
+ self.qmui_view.qmui_badgeTextColor = qmui_badgeTextColor;
}
- (UIColor *)qmui_badgeTextColor {
@@ -178,10 +116,7 @@ - (UIColor *)qmui_badgeTextColor {
static char kAssociatedObjectKey_badgeFont;
- (void)setQmui_badgeFont:(UIFont *)qmui_badgeFont {
objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeFont, qmui_badgeFont, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_badgeLabel) {
- self.qmui_badgeLabel.font = qmui_badgeFont;
- [self setNeedsUpdateBadgeLabelLayout];
- }
+ self.qmui_view.qmui_badgeFont = qmui_badgeFont;
}
- (UIFont *)qmui_badgeFont {
@@ -191,53 +126,47 @@ - (UIFont *)qmui_badgeFont {
static char kAssociatedObjectKey_badgeContentEdgeInsets;
- (void)setQmui_badgeContentEdgeInsets:(UIEdgeInsets)qmui_badgeContentEdgeInsets {
objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets, [NSValue valueWithUIEdgeInsets:qmui_badgeContentEdgeInsets], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_badgeLabel) {
- self.qmui_badgeLabel.contentEdgeInsets = qmui_badgeContentEdgeInsets;
- [self setNeedsUpdateBadgeLabelLayout];
- }
+ self.qmui_view.qmui_badgeContentEdgeInsets = qmui_badgeContentEdgeInsets;
}
- (UIEdgeInsets)qmui_badgeContentEdgeInsets {
return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets)) UIEdgeInsetsValue];
}
-static char kAssociatedObjectKey_badgeCenterOffset;
-- (void)setQmui_badgeCenterOffset:(CGPoint)qmui_badgeCenterOffset {
- objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffset, [NSValue valueWithCGPoint:qmui_badgeCenterOffset], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_badgeLabel) {
- self.qmui_badgeLabel.centerOffset = qmui_badgeCenterOffset;
- }
+static char kAssociatedObjectKey_badgeOffset;
+- (void)setQmui_badgeOffset:(CGPoint)qmui_badgeOffset {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffset, @(qmui_badgeOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ self.qmui_view.qmui_badgeOffset = qmui_badgeOffset;
}
-- (CGPoint)qmui_badgeCenterOffset {
- return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffset)) CGPointValue];
+- (CGPoint)qmui_badgeOffset {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffset)) CGPointValue];
}
-static char kAssociatedObjectKey_badgeCenterOffsetLandscape;
-- (void)setQmui_badgeCenterOffsetLandscape:(CGPoint)qmui_badgeCenterOffsetLandscape {
- objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffsetLandscape, [NSValue valueWithCGPoint:qmui_badgeCenterOffsetLandscape], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_badgeLabel) {
- self.qmui_badgeLabel.centerOffsetLandscape = qmui_badgeCenterOffsetLandscape;
- }
+static char kAssociatedObjectKey_badgeOffsetLandscape;
+- (void)setQmui_badgeOffsetLandscape:(CGPoint)qmui_badgeOffsetLandscape {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape, @(qmui_badgeOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ self.qmui_view.qmui_badgeOffsetLandscape = qmui_badgeOffsetLandscape;
}
-- (CGPoint)qmui_badgeCenterOffsetLandscape {
- return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffsetLandscape)) CGPointValue];
+- (CGPoint)qmui_badgeOffsetLandscape {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape)) CGPointValue];
}
-static char kAssociatedObjectKey_badgeLabel;
-- (void)setQmui_badgeLabel:(UILabel *)qmui_badgeLabel {
- objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeLabel, qmui_badgeLabel, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+- (void)setQmui_badgeView:(__kindof UIView *)qmui_badgeView {
+ self.qmui_view.qmui_badgeView = qmui_badgeView;
}
-- (_QMUIBadgeLabel *)qmui_badgeLabel {
- return (_QMUIBadgeLabel *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeLabel);
+- (__kindof UIView *)qmui_badgeView {
+ return self.qmui_view.qmui_badgeView;
}
-- (void)setNeedsUpdateBadgeLabelLayout {
- if (self.qmui_badgeString.length) {
- [self.qmui_view setNeedsLayout];
- }
+- (void)setQmui_badgeViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_badgeViewDidLayoutBlock {
+ self.qmui_view.qmui_badgeViewDidLayoutBlock = qmui_badgeViewDidLayoutBlock;
+}
+
+- (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_badgeViewDidLayoutBlock {
+ return self.qmui_view.qmui_badgeViewDidLayoutBlock;
}
#pragma mark - UpdatesIndicator
@@ -246,35 +175,9 @@ - (void)setNeedsUpdateBadgeLabelLayout {
- (void)setQmui_shouldShowUpdatesIndicator:(BOOL)qmui_shouldShowUpdatesIndicator {
objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowUpdatesIndicator, @(qmui_shouldShowUpdatesIndicator), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (qmui_shouldShowUpdatesIndicator) {
- if (!self.qmui_updatesIndicatorView) {
- self.qmui_updatesIndicatorView = [[_QMUIUpdatesIndicatorView alloc] qmui_initWithSize:self.qmui_updatesIndicatorSize];
- self.qmui_updatesIndicatorView.layer.cornerRadius = CGRectGetHeight(self.qmui_updatesIndicatorView.bounds) / 2;
- self.qmui_updatesIndicatorView.backgroundColor = self.qmui_updatesIndicatorColor;
- self.qmui_updatesIndicatorView.centerOffset = self.qmui_updatesIndicatorCenterOffset;
- self.qmui_updatesIndicatorView.centerOffsetLandscape = self.qmui_updatesIndicatorCenterOffsetLandscape;
- if (!self.qmui_viewDidLayoutSubviewsBlock) {
- self.qmui_viewDidLayoutSubviewsBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) {
- [item layoutSubviews];
- };
- }
- if (!self.qmui_viewDidSetBlock) {
- self.qmui_viewDidSetBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) {
- [view addSubview:item.qmui_updatesIndicatorView];
- [view addSubview:item.qmui_badgeLabel];
- [view setNeedsLayout];
- [view layoutIfNeeded];
- };
- }
- // 之前 item 已经 set 完 view,则手动触发一次
- if (self.qmui_view) {
- self.qmui_viewDidSetBlock(self, self.qmui_view);
- }
- }
- [self setNeedsUpdateIndicatorLayout];
- self.qmui_updatesIndicatorView.hidden = NO;
- } else {
- self.qmui_updatesIndicatorView.hidden = YES;
+ [self updateViewDidSetBlockIfNeeded];
}
+ self.qmui_view.qmui_shouldShowUpdatesIndicator = qmui_shouldShowUpdatesIndicator;
}
- (BOOL)qmui_shouldShowUpdatesIndicator {
@@ -284,9 +187,7 @@ - (BOOL)qmui_shouldShowUpdatesIndicator {
static char kAssociatedObjectKey_updatesIndicatorColor;
- (void)setQmui_updatesIndicatorColor:(UIColor *)qmui_updatesIndicatorColor {
objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorColor, qmui_updatesIndicatorColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_updatesIndicatorView) {
- self.qmui_updatesIndicatorView.backgroundColor = qmui_updatesIndicatorColor;
- }
+ self.qmui_view.qmui_updatesIndicatorColor = qmui_updatesIndicatorColor;
}
- (UIColor *)qmui_updatesIndicatorColor {
@@ -296,146 +197,75 @@ - (UIColor *)qmui_updatesIndicatorColor {
static char kAssociatedObjectKey_updatesIndicatorSize;
- (void)setQmui_updatesIndicatorSize:(CGSize)qmui_updatesIndicatorSize {
objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize, [NSValue valueWithCGSize:qmui_updatesIndicatorSize], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_updatesIndicatorView) {
- self.qmui_updatesIndicatorView.frame = CGRectSetSize(self.qmui_updatesIndicatorView.frame, qmui_updatesIndicatorSize);
- self.qmui_updatesIndicatorView.layer.cornerRadius = qmui_updatesIndicatorSize.height / 2;
- [self setNeedsUpdateIndicatorLayout];
- }
+ self.qmui_view.qmui_updatesIndicatorSize = qmui_updatesIndicatorSize;
}
- (CGSize)qmui_updatesIndicatorSize {
return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize)) CGSizeValue];
}
-static char kAssociatedObjectKey_updatesIndicatorCenterOffset;
-- (void)setQmui_updatesIndicatorCenterOffset:(CGPoint)qmui_updatesIndicatorCenterOffset {
- objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffset, [NSValue valueWithCGPoint:qmui_updatesIndicatorCenterOffset], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_updatesIndicatorView) {
- self.qmui_updatesIndicatorView.centerOffset = qmui_updatesIndicatorCenterOffset;
- [self setNeedsUpdateIndicatorLayout];
- }
+static char kAssociatedObjectKey_updatesIndicatorOffset;
+- (void)setQmui_updatesIndicatorOffset:(CGPoint)qmui_updatesIndicatorOffset {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset, @(qmui_updatesIndicatorOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ self.qmui_view.qmui_updatesIndicatorOffset = qmui_updatesIndicatorOffset;
}
-- (CGPoint)qmui_updatesIndicatorCenterOffset {
- return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffset)) CGPointValue];
+- (CGPoint)qmui_updatesIndicatorOffset {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset)) CGPointValue];
}
-static char kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape;
-- (void)setQmui_updatesIndicatorCenterOffsetLandscape:(CGPoint)qmui_updatesIndicatorCenterOffsetLandscape {
- objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape, [NSValue valueWithCGPoint:qmui_updatesIndicatorCenterOffsetLandscape], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- if (self.qmui_updatesIndicatorView) {
- self.qmui_updatesIndicatorView.centerOffsetLandscape = qmui_updatesIndicatorCenterOffsetLandscape;
- [self setNeedsUpdateIndicatorLayout];
- }
+static char kAssociatedObjectKey_updatesIndicatorOffsetLandscape;
+- (void)setQmui_updatesIndicatorOffsetLandscape:(CGPoint)qmui_updatesIndicatorOffsetLandscape {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape, @(qmui_updatesIndicatorOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ self.qmui_view.qmui_updatesIndicatorOffsetLandscape = qmui_updatesIndicatorOffsetLandscape;
}
-- (CGPoint)qmui_updatesIndicatorCenterOffsetLandscape {
- return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape)) CGPointValue];
+- (CGPoint)qmui_updatesIndicatorOffsetLandscape {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape)) CGPointValue];
}
-static char kAssociatedObjectKey_updatesIndicatorView;
-- (void)setQmui_updatesIndicatorView:(UIView *)qmui_updatesIndicatorView {
- objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView, qmui_updatesIndicatorView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+- (void)setQmui_updatesIndicatorView:(__kindof UIView *)qmui_updatesIndicatorView {
+ self.qmui_view.qmui_updatesIndicatorView = qmui_updatesIndicatorView;
}
-- (_QMUIUpdatesIndicatorView *)qmui_updatesIndicatorView {
- return (_QMUIUpdatesIndicatorView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView);
+- (UIView *)qmui_updatesIndicatorView {
+ return self.qmui_view.qmui_updatesIndicatorView;
}
-- (void)setNeedsUpdateIndicatorLayout {
- if (self.qmui_shouldShowUpdatesIndicator) {
- [self.qmui_view setNeedsLayout];
- }
+- (void)setQmui_updatesIndicatorViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock {
+ self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock = qmui_updatesIndicatorViewDidLayoutBlock;
}
-#pragma mark - Common
-
-- (void)layoutSubviews {
-
- if (self.qmui_updatesIndicatorView && !self.qmui_updatesIndicatorView.hidden) {
- CGPoint centerOffset = IS_LANDSCAPE ? self.qmui_updatesIndicatorView.centerOffsetLandscape : self.qmui_updatesIndicatorView.centerOffset;
-
- UIView *superview = self.qmui_updatesIndicatorView.superview;
- if ([NSStringFromClass(superview.class) hasPrefix:@"UITabBar"]) {
- // 特别的,对于 UITabBarItem,将 imageView 的 center 作为参考点
- UIView *imageView = [UITabBarItem qmui_imageViewInTabBarButton:superview];
- if (!imageView) return;
+- (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock {
+ return self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock;
+}
- self.qmui_updatesIndicatorView.frame = CGRectSetXY(self.qmui_updatesIndicatorView.frame, CGRectGetMinXHorizontallyCenter(imageView.frame, self.qmui_updatesIndicatorView.frame) + centerOffset.x, CGRectGetMinYVerticallyCenter(imageView.frame, self.qmui_updatesIndicatorView.frame) + centerOffset.y);
- } else {
- self.qmui_updatesIndicatorView.frame = CGRectSetXY(self.qmui_updatesIndicatorView.frame, CGFloatGetCenter(superview.qmui_width, self.qmui_updatesIndicatorView.qmui_width) + centerOffset.x, CGFloatGetCenter(superview.qmui_height, self.qmui_updatesIndicatorView.qmui_height) + centerOffset.y);
- }
+#pragma mark - Common
- [superview bringSubviewToFront:self.qmui_updatesIndicatorView];
- }
-
- if (self.qmui_badgeLabel && !self.qmui_badgeLabel.hidden) {
- [self.qmui_badgeLabel sizeToFit];
- self.qmui_badgeLabel.layer.cornerRadius = MIN(self.qmui_badgeLabel.qmui_height / 2, self.qmui_badgeLabel.qmui_width / 2);
-
- CGPoint centerOffset = IS_LANDSCAPE ? self.qmui_badgeLabel.centerOffsetLandscape : self.qmui_badgeLabel.centerOffset;
-
- UIView *superview = self.qmui_badgeLabel.superview;
- if ([NSStringFromClass(superview.class) hasPrefix:@"UITabBar"]) {
- // 特别的,对于 UITabBarItem,将 imageView 的 center 作为参考点
- UIView *imageView = [UITabBarItem qmui_imageViewInTabBarButton:superview];
- if (!imageView) return;
+- (void)updateViewDidSetBlockIfNeeded {
+ if (!self.qmui_viewDidSetBlock) {
+ self.qmui_viewDidSetBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) {
+ view.qmui_badgeBackgroundColor = item.qmui_badgeBackgroundColor;
+ view.qmui_badgeTextColor = item.qmui_badgeTextColor;
+ view.qmui_badgeFont = item.qmui_badgeFont;
+ view.qmui_badgeContentEdgeInsets = item.qmui_badgeContentEdgeInsets;
+ view.qmui_badgeOffset = item.qmui_badgeOffset;
+ view.qmui_badgeOffsetLandscape = item.qmui_badgeOffsetLandscape;
- self.qmui_badgeLabel.frame = CGRectSetXY(self.qmui_badgeLabel.frame, CGRectGetMinXHorizontallyCenter(imageView.frame, self.qmui_badgeLabel.frame) + centerOffset.x, CGRectGetMinYVerticallyCenter(imageView.frame, self.qmui_badgeLabel.frame) + centerOffset.y);
- } else {
- self.qmui_badgeLabel.frame = CGRectSetXY(self.qmui_badgeLabel.frame, CGFloatGetCenter(superview.qmui_width, self.qmui_badgeLabel.qmui_width) + centerOffset.x, CGFloatGetCenter(superview.qmui_height, self.qmui_badgeLabel.qmui_height) + centerOffset.y);
- }
+ view.qmui_updatesIndicatorColor = item.qmui_updatesIndicatorColor;
+ view.qmui_updatesIndicatorSize = item.qmui_updatesIndicatorSize;
+ view.qmui_updatesIndicatorOffset = item.qmui_updatesIndicatorOffset;
+ view.qmui_updatesIndicatorOffsetLandscape = item.qmui_updatesIndicatorOffsetLandscape;
+
+ view.qmui_badgeString = item.qmui_badgeString;
+ view.qmui_shouldShowUpdatesIndicator = item.qmui_shouldShowUpdatesIndicator;
+ };
- [superview bringSubviewToFront:self.qmui_badgeLabel];
- }
-}
-
-@end
-
-@implementation _QMUIUpdatesIndicatorView
-
-- (void)setCenterOffset:(CGPoint)centerOffset {
- _centerOffset = centerOffset;
- if (!IS_LANDSCAPE) {
- [self.superview setNeedsLayout];
- }
-}
-
-- (void)setCenterOffsetLandscape:(CGPoint)centerOffsetLandscape {
- _centerOffsetLandscape = centerOffsetLandscape;
- if (IS_LANDSCAPE) {
- [self.superview setNeedsLayout];
- }
-}
-
-@end
-
-@implementation _QMUIBadgeLabel
-
-- (void)setCenterOffset:(CGPoint)centerOffset {
- _centerOffset = centerOffset;
- if (!IS_LANDSCAPE) {
- [self.superview setNeedsLayout];
- }
-}
-
-- (void)setCenterOffsetLandscape:(CGPoint)centerOffsetLandscape {
- _centerOffsetLandscape = centerOffsetLandscape;
- if (IS_LANDSCAPE) {
- [self.superview setNeedsLayout];
- }
-}
-
-- (CGSize)sizeThatFits:(CGSize)size {
- CGSize result = [super sizeThatFits:size];
-
- // 只有一个字的时候保证它是一个正方形
- if (self.text.length <= 1) {
- CGFloat finalSize = MAX(result.width, result.height);
- result = CGSizeMake(finalSize, finalSize);
+ // 为 qmui_viewDidSetBlock 赋值前 item 已经 set 完 view,则手动触发一次
+ if (self.qmui_view) {
+ self.qmui_viewDidSetBlock(self, self.qmui_view);
+ }
}
-
- return result;
}
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.h b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.h
new file mode 100644
index 00000000..b56b7bc4
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.h
@@ -0,0 +1,30 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// UIView+QMUIBadge.h
+// QMUIKit
+//
+// Created by MoLice on 2020/5/26.
+//
+
+#import
+#import "QMUIBadgeProtocol.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ 用于在任意 UIView 上显示未读红点或者未读数,提供的属性请查看 @c QMUIBadgeProtocol ,属性的默认值在 QMUIConfigurationTemplate 配置表里设置,如果不使用配置表,则所有属性的默认值均为 0 或 nil。
+
+ @note 使用该组件会强制设置 view.clipsToBounds = NO 以避免布局到 view 外部的红点/未读数看不到。
+ */
+@interface UIView (QMUIBadge)
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m
new file mode 100644
index 00000000..1bb929a5
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m
@@ -0,0 +1,376 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// UIView+QMUIBadge.m
+// QMUIKit
+//
+// Created by MoLice on 2020/5/26.
+//
+
+#import "UIView+QMUIBadge.h"
+#import "QMUICore.h"
+#import "QMUILabel.h"
+#import "UIView+QMUI.h"
+#import "UITabBarItem+QMUI.h"
+#import "QMUIBadgeLabel.h"
+
+@interface UIView ()
+@property(nullable, nonatomic, strong) void (^qmuibdg_layoutSubviewsBlock)(__kindof UIView *view);
+@end
+
+@implementation UIView (QMUIBadge)
+
+QMUISynthesizeIdStrongProperty(qmuibdg_layoutSubviewsBlock, setQmuibdg_layoutSubviewsBlock)
+QMUISynthesizeIdCopyProperty(qmui_badgeViewDidLayoutBlock, setQmui_badgeViewDidLayoutBlock)
+QMUISynthesizeIdCopyProperty(qmui_updatesIndicatorViewDidLayoutBlock, setQmui_updatesIndicatorViewDidLayoutBlock)
+
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ // 保证配置表里的默认值正确被设置
+ ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithFrame:), CGRect, UIView *, ^UIView *(UIView *selfObject, CGRect firstArgv, UIView *originReturnValue) {
+ [selfObject qmuibdg_didInitialize];
+ return originReturnValue;
+ });
+
+ ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithCoder:), NSCoder *, UIView *, ^UIView *(UIView *selfObject, NSCoder *firstArgv, UIView *originReturnValue) {
+ [selfObject qmuibdg_didInitialize];
+ return originReturnValue;
+ });
+
+ OverrideImplementation([UIView class], @selector(setQmui_layoutSubviewsBlock:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UIView *selfObject, void (^firstArgv)(__kindof UIView *aView)) {
+
+ if (firstArgv && selfObject.qmuibdg_layoutSubviewsBlock && firstArgv != selfObject.qmuibdg_layoutSubviewsBlock) {
+ firstArgv = ^void(__kindof UIView *aaView) {
+ firstArgv(aaView);
+ aaView.qmuibdg_layoutSubviewsBlock(aaView);
+ };
+ }
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, void (^firstArgv)(__kindof UIView *aView));
+ originSelectorIMP = (void (*)(id, SEL, void (^firstArgv)(__kindof UIView *aView)))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, firstArgv);
+ };
+ });
+ });
+}
+
+- (void)qmuibdg_didInitialize {
+ if (QMUICMIActivated) {
+ self.qmui_badgeBackgroundColor = BadgeBackgroundColor;
+ self.qmui_badgeTextColor = BadgeTextColor;
+ self.qmui_badgeFont = BadgeFont;
+ self.qmui_badgeContentEdgeInsets = BadgeContentEdgeInsets;
+ self.qmui_badgeOffset = BadgeOffset;
+ self.qmui_badgeOffsetLandscape = BadgeOffsetLandscape;
+
+ self.qmui_updatesIndicatorColor = UpdatesIndicatorColor;
+ self.qmui_updatesIndicatorSize = UpdatesIndicatorSize;
+ self.qmui_updatesIndicatorOffset = UpdatesIndicatorOffset;
+ self.qmui_updatesIndicatorOffsetLandscape = UpdatesIndicatorOffsetLandscape;
+ }
+}
+
+#pragma mark - Badge
+
+static char kAssociatedObjectKey_badgeInteger;
+- (void)setQmui_badgeInteger:(NSUInteger)qmui_badgeInteger {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeInteger, @(qmui_badgeInteger), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ self.qmui_badgeString = qmui_badgeInteger > 0 ? [NSString stringWithFormat:@"%@", @(qmui_badgeInteger)] : nil;
+}
+
+- (NSUInteger)qmui_badgeInteger {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeInteger)) unsignedIntegerValue];
+}
+
+static char kAssociatedObjectKey_badgeString;
+- (void)setQmui_badgeString:(NSString *)qmui_badgeString {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeString, qmui_badgeString, OBJC_ASSOCIATION_COPY_NONATOMIC);
+ if (qmui_badgeString.length) {
+ if (!self.qmui_badgeView) {
+ QMUIBadgeLabel *badgeLabel = [[QMUIBadgeLabel alloc] init];
+ badgeLabel.backgroundColor = self.qmui_badgeBackgroundColor;
+ badgeLabel.textColor = self.qmui_badgeTextColor;
+ badgeLabel.font = self.qmui_badgeFont;
+ badgeLabel.contentEdgeInsets = self.qmui_badgeContentEdgeInsets;
+ self.qmui_badgeView = badgeLabel;
+ }
+ if ([self.qmui_badgeView respondsToSelector:@selector(setText:)]) {
+ ((UILabel *)self.qmui_badgeView).text = qmui_badgeString;
+ }
+ self.qmui_badgeView.hidden = NO;
+ [self setNeedsUpdateBadgeLabelLayout];
+ QMUIAssert(!self.clipsToBounds, @"QMUIBadge", @"clipsToBounds should be NO when showing badgeString");
+ self.clipsToBounds = NO;
+ } else {
+ self.qmui_badgeView.hidden = YES;
+ }
+}
+
+- (NSString *)qmui_badgeString {
+ return (NSString *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeString);
+}
+
+static char kAssociatedObjectKey_badgeBackgroundColor;
+- (void)setQmui_badgeBackgroundColor:(UIColor *)qmui_badgeBackgroundColor {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeBackgroundColor, qmui_badgeBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ self.qmui_badgeView.backgroundColor = qmui_badgeBackgroundColor;
+}
+
+- (UIColor *)qmui_badgeBackgroundColor {
+ return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeBackgroundColor);
+}
+
+static char kAssociatedObjectKey_badgeTextColor;
+- (void)setQmui_badgeTextColor:(UIColor *)qmui_badgeTextColor {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeTextColor, qmui_badgeTextColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if ([self.qmui_badgeView isKindOfClass:UILabel.class]) {
+ ((UILabel *)self.qmui_badgeView).textColor = qmui_badgeTextColor;
+ }
+}
+
+- (UIColor *)qmui_badgeTextColor {
+ return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeTextColor);
+}
+
+static char kAssociatedObjectKey_badgeFont;
+- (void)setQmui_badgeFont:(UIFont *)qmui_badgeFont {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeFont, qmui_badgeFont, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if ([self.qmui_badgeView isKindOfClass:UILabel.class]) {
+ ((UILabel *)self.qmui_badgeView).font = qmui_badgeFont;
+ [self setNeedsUpdateBadgeLabelLayout];
+ }
+}
+
+- (UIFont *)qmui_badgeFont {
+ return (UIFont *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeFont);
+}
+
+static char kAssociatedObjectKey_badgeContentEdgeInsets;
+- (void)setQmui_badgeContentEdgeInsets:(UIEdgeInsets)qmui_badgeContentEdgeInsets {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets, [NSValue valueWithUIEdgeInsets:qmui_badgeContentEdgeInsets], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if ([self.qmui_badgeView isKindOfClass:QMUILabel.class]) {
+ ((QMUILabel *)self.qmui_badgeView).contentEdgeInsets = qmui_badgeContentEdgeInsets;
+ [self setNeedsUpdateBadgeLabelLayout];
+ }
+}
+
+- (UIEdgeInsets)qmui_badgeContentEdgeInsets {
+ return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets)) UIEdgeInsetsValue];
+}
+
+static char kAssociatedObjectKey_badgeOffset;
+- (void)setQmui_badgeOffset:(CGPoint)qmui_badgeOffset {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffset, @(qmui_badgeOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ [self setNeedsUpdateBadgeLabelLayout];
+}
+
+- (CGPoint)qmui_badgeOffset {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffset)) CGPointValue];
+}
+
+static char kAssociatedObjectKey_badgeOffsetLandscape;
+- (void)setQmui_badgeOffsetLandscape:(CGPoint)qmui_badgeOffsetLandscape {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape, @(qmui_badgeOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ [self setNeedsUpdateBadgeLabelLayout];
+}
+
+- (CGPoint)qmui_badgeOffsetLandscape {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape)) CGPointValue];
+}
+
+static char kAssociatedObjectKey_badgeView;
+- (void)setQmui_badgeView:(UIView *)qmui_badgeView {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeView, qmui_badgeView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (qmui_badgeView) {
+ [self updateLayoutSubviewsBlockIfNeeded];
+ [self addSubview:qmui_badgeView];
+ [self setNeedsUpdateBadgeLabelLayout];
+ }
+}
+
+- (__kindof UIView *)qmui_badgeView {
+ return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeView);
+}
+
+- (void)setNeedsUpdateBadgeLabelLayout {
+ if (self.qmui_badgeView && !self.qmui_badgeView.hidden) {
+ [self qmuibdg_layoutSubviews];
+ }
+}
+
+#pragma mark - UpdatesIndicator
+
+static char kAssociatedObjectKey_shouldShowUpdatesIndicator;
+- (void)setQmui_shouldShowUpdatesIndicator:(BOOL)qmui_shouldShowUpdatesIndicator {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowUpdatesIndicator, @(qmui_shouldShowUpdatesIndicator), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (qmui_shouldShowUpdatesIndicator) {
+ if (!self.qmui_updatesIndicatorView) {
+ self.qmui_updatesIndicatorView = [[UIView alloc] qmui_initWithSize:self.qmui_updatesIndicatorSize];
+ self.qmui_updatesIndicatorView.layer.cornerRadius = CGRectGetHeight(self.qmui_updatesIndicatorView.bounds) / 2;
+ self.qmui_updatesIndicatorView.backgroundColor = self.qmui_updatesIndicatorColor;
+ }
+ [self setNeedsUpdateIndicatorLayout];
+ QMUIAssert(!self.clipsToBounds, @"QMUIBadge", @"clipsToBounds should be NO when showing updatesIndicator");
+ self.clipsToBounds = NO;
+ self.qmui_updatesIndicatorView.hidden = NO;
+ } else {
+ self.qmui_updatesIndicatorView.hidden = YES;
+ }
+}
+
+- (BOOL)qmui_shouldShowUpdatesIndicator {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_shouldShowUpdatesIndicator)) boolValue];
+}
+
+static char kAssociatedObjectKey_updatesIndicatorColor;
+- (void)setQmui_updatesIndicatorColor:(UIColor *)qmui_updatesIndicatorColor {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorColor, qmui_updatesIndicatorColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ self.qmui_updatesIndicatorView.backgroundColor = qmui_updatesIndicatorColor;
+}
+
+- (UIColor *)qmui_updatesIndicatorColor {
+ return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorColor);
+}
+
+static char kAssociatedObjectKey_updatesIndicatorSize;
+- (void)setQmui_updatesIndicatorSize:(CGSize)qmui_updatesIndicatorSize {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize, [NSValue valueWithCGSize:qmui_updatesIndicatorSize], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (self.qmui_updatesIndicatorView) {
+ self.qmui_updatesIndicatorView.frame = CGRectSetSize(self.qmui_updatesIndicatorView.frame, qmui_updatesIndicatorSize);
+ self.qmui_updatesIndicatorView.layer.cornerRadius = qmui_updatesIndicatorSize.height / 2;
+ [self setNeedsUpdateIndicatorLayout];
+ }
+}
+
+- (CGSize)qmui_updatesIndicatorSize {
+ return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize)) CGSizeValue];
+}
+
+static char kAssociatedObjectKey_updatesIndicatorOffset;
+- (void)setQmui_updatesIndicatorOffset:(CGPoint)qmui_updatesIndicatorOffset {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset, @(qmui_updatesIndicatorOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (self.qmui_updatesIndicatorView) {
+ [self setNeedsUpdateIndicatorLayout];
+ }
+}
+
+- (CGPoint)qmui_updatesIndicatorOffset {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset)) CGPointValue];
+}
+
+static char kAssociatedObjectKey_updatesIndicatorOffsetLandscape;
+- (void)setQmui_updatesIndicatorOffsetLandscape:(CGPoint)qmui_updatesIndicatorOffsetLandscape {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape, @(qmui_updatesIndicatorOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (self.qmui_updatesIndicatorView) {
+ [self setNeedsUpdateIndicatorLayout];
+ }
+}
+
+- (CGPoint)qmui_updatesIndicatorOffsetLandscape {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape)) CGPointValue];
+}
+
+static char kAssociatedObjectKey_updatesIndicatorView;
+- (void)setQmui_updatesIndicatorView:(__kindof UIView *)qmui_updatesIndicatorView {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView, qmui_updatesIndicatorView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (qmui_updatesIndicatorView) {
+ [self updateLayoutSubviewsBlockIfNeeded];
+ [self addSubview:qmui_updatesIndicatorView];
+ [self setNeedsUpdateIndicatorLayout];
+ }
+}
+
+- (__kindof UIView *)qmui_updatesIndicatorView {
+ return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView);
+}
+
+- (void)setNeedsUpdateIndicatorLayout {
+ if (self.qmui_shouldShowUpdatesIndicator) {
+ [self qmuibdg_layoutSubviews];
+ }
+}
+
+#pragma mark - Common
+
+- (void)updateLayoutSubviewsBlockIfNeeded {
+ if (!self.qmuibdg_layoutSubviewsBlock) {
+ self.qmuibdg_layoutSubviewsBlock = ^(UIView *view) {
+ [view qmuibdg_layoutSubviews];
+ };
+ }
+ if (!self.qmui_layoutSubviewsBlock) {
+ self.qmui_layoutSubviewsBlock = self.qmuibdg_layoutSubviewsBlock;
+ } else if (self.qmui_layoutSubviewsBlock != self.qmuibdg_layoutSubviewsBlock) {
+ void (^originalLayoutSubviewsBlock)(__kindof UIView *) = self.qmui_layoutSubviewsBlock;
+ self.qmuibdg_layoutSubviewsBlock = ^(__kindof UIView *view) {
+ originalLayoutSubviewsBlock(view);
+ [view qmuibdg_layoutSubviews];
+ };
+ self.qmui_layoutSubviewsBlock = self.qmuibdg_layoutSubviewsBlock;
+ }
+}
+
+// 不管 image 还是 text 的 UIBarButtonItem 都获取内部的 _UIModernBarButton 即可
+- (UIView *)findBarButtonContentView {
+ NSString *classString = NSStringFromClass(self.class);
+ if ([classString isEqualToString:@"UITabBarButton"]) {
+ // 特别的,对于 UITabBarItem,将 imageView 作为参考 view
+ UIView *imageView = [UITabBarItem qmui_imageViewInTabBarButton:self];
+ return imageView;
+ }
+
+ if ([classString isEqualToString:@"_UIButtonBarButton"]) {
+ for (UIView *subview in self.subviews) {
+ if ([subview isKindOfClass:UIButton.class]) {
+ return subview;
+ }
+ }
+ }
+
+ return nil;
+}
+
+- (void)qmuibdg_layoutSubviews {
+
+ void (^layoutBlock)(UIView *view, UIView *badgeView) = ^void(UIView *view, UIView *badgeView) {
+ BeginIgnoreDeprecatedWarning
+ CGPoint offset = badgeView == view.qmui_badgeView
+ ? (IS_LANDSCAPE ? view.qmui_badgeOffsetLandscape : view.qmui_badgeOffset)
+ : (IS_LANDSCAPE ? view.qmui_updatesIndicatorOffsetLandscape : view.qmui_updatesIndicatorOffset);
+ EndIgnoreDeprecatedWarning
+
+ UIView *contentView = [view findBarButtonContentView];
+ if (contentView) {
+ CGRect imageViewFrame = [view convertRect:contentView.frame fromView:contentView.superview];
+ badgeView.frame = CGRectSetXY(badgeView.frame, CGRectGetMaxX(imageViewFrame) + offset.x, CGRectGetMinY(imageViewFrame) - CGRectGetHeight(badgeView.frame) + offset.y);
+ } else {
+ badgeView.frame = CGRectSetXY(badgeView.frame, CGRectGetWidth(view.bounds) + offset.x, - CGRectGetHeight(badgeView.frame) + offset.y);
+ }
+ [view bringSubviewToFront:badgeView];
+ };
+
+ if (self.qmui_updatesIndicatorView && !self.qmui_updatesIndicatorView.hidden) {
+ layoutBlock(self, self.qmui_updatesIndicatorView);
+ if (self.qmui_updatesIndicatorViewDidLayoutBlock) {
+ self.qmui_updatesIndicatorViewDidLayoutBlock(self, self.qmui_updatesIndicatorView);
+ }
+ }
+ if (self.qmui_badgeView && !self.qmui_badgeView.hidden) {
+ [self.qmui_badgeView sizeToFit];
+ layoutBlock(self, self.qmui_badgeView);
+ if (self.qmui_badgeViewDidLayoutBlock) {
+ self.qmui_badgeViewDidLayoutBlock(self, self.qmui_badgeView);
+ }
+ }
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h
index bc7d4ad7..88f9ed47 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIButton.h
@@ -25,14 +25,20 @@ typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) {
QMUIButtonImagePositionRight, // imageView在titleLabel右边
};
+/**
+ * 用于 `QMUIButton.cornerRadius` 属性,当 `cornerRadius` 为 `QMUIButtonCornerRadiusAdjustsBounds` 时,`QMUIButton` 会在高度变化时自动调整 `cornerRadius`,使其始终保持为高度的 1/2。
+ */
+extern const CGFloat QMUIButtonCornerRadiusAdjustsBounds;
+
/**
* 提供以下功能:
- * 1. 支持让文字和图片自动跟随 tintColor 变化(系统的 UIButton 默认是不响应 tintColor 的)
- * 2. highlighted、disabled 状态均通过改变整个按钮的alpha来表现,无需分别设置不同 state 下的 titleColor、image。alpha 的值可在配置表里修改 ButtonHighlightedAlpha、ButtonDisabledAlpha。
- * 3. 支持点击时改变背景色颜色(highlightedBackgroundColor)
- * 4. 支持点击时改变边框颜色(highlightedBorderColor)
- * 5. 支持设置图片相对于 titleLabel 的位置(imagePosition)
- * 6. 支持设置图片和 titleLabel 之间的间距,无需自行调整 titleEdgeInests、imageEdgeInsets(spacingBetweenImageAndTitle)
+ * 1. 支持让文字和图片自动跟随 tintColor 变化(系统的 UIButton 默认是不响应 tintColor 的)。
+ * 2. 支持自动将圆角值保持为按钮高度的一半。
+ * 3. highlighted、disabled 状态均通过改变整个按钮的alpha来表现,无需分别设置不同 state 下的 titleColor、image。alpha 的值可在配置表里修改 ButtonHighlightedAlpha、ButtonDisabledAlpha。
+ * 4. 支持点击时改变背景色颜色(highlightedBackgroundColor)。
+ * 5. 支持点击时改变边框颜色(highlightedBorderColor)。
+ * 6. 支持设置图片相对于 titleLabel 的位置(imagePosition)。
+ * 7. 支持设置图片和 titleLabel 之间的间距,无需自行调整 titleEdgeInests、imageEdgeInsets(spacingBetweenImageAndTitle)。
* @warning QMUIButton 重新定义了 UIButton.titleEdgeInests、imageEdgeInsets、contentEdgeInsets 这三者的布局逻辑,sizeThatFits: 里会把 titleEdgeInests 和 imageEdgeInsets 也考虑在内(UIButton 不会),以使这三个接口的使用更符合直觉。
*/
@interface QMUIButton : UIButton
@@ -42,6 +48,11 @@ typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) {
*/
- (void)didInitialize NS_REQUIRES_SUPER;
+@property(nonatomic, strong, nullable) NSString *subtitle;
+@property(nonatomic, strong, readonly) UILabel *subtitleLabel;
+@property(nonatomic, assign) IBInspectable UIEdgeInsets subtitleEdgeInsets;
+@property(nonatomic, strong, nullable) IBInspectable UIColor *subtitleColor;
+
/**
* 让按钮的文字颜色自动跟随tintColor调整(系统默认titleColor是不跟随的)
* 默认为NO
@@ -56,7 +67,6 @@ typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) {
/**
* 等价于 adjustsTitleTintColorAutomatically = YES & adjustsImageTintColorAutomatically = YES & tintColor = xxx
- * @note 一般只使用这个属性的 setter,而 getter 永远返回 self.tintColor
* @warning 不支持传 nil
*/
@property(nonatomic, strong) IBInspectable UIColor *tintColorAdjustsTitleAndImage;
@@ -100,6 +110,8 @@ typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) {
*/
@property(nonatomic, assign) IBInspectable CGFloat spacingBetweenImageAndTitle;
+@property(nonatomic, assign) IBInspectable CGFloat cornerRadius UI_APPEARANCE_SELECTOR;// 默认为 0。将其设置为 QMUIButtonCornerRadiusAdjustsBounds 可自动保持圆角为按钮高度的一半。
+
@end
NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m
index dc800455..28944c00 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIButton.m
@@ -15,9 +15,11 @@
#import "QMUIButton.h"
#import "QMUICore.h"
-#import "QMUILog.h"
#import "CALayer+QMUI.h"
#import "UIButton+QMUI.h"
+#import "QMUILayouter.h"
+
+const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
@interface QMUIButton ()
@@ -27,17 +29,19 @@ @interface QMUIButton ()
@implementation QMUIButton
+@synthesize subtitleLabel = _qmuisubtitleLabel;
+
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
- [self didInitialize];
-
self.tintColor = ButtonTintColor;
- if (!self.adjustsTitleTintColorAutomatically) {
- [self setTitleColor:self.tintColor forState:UIControlStateNormal];
- }
+ [self setTitleColor:self.tintColor forState:UIControlStateNormal];// 初始化时 adjustsTitleTintColorAutomatically 还是 NO,所以这里手动把 titleColor 设置为 tintColor 的值
+ self.subtitleColor = self.tintColor;
// iOS7以后的button,sizeToFit后默认会自带一个上下的contentInsets,为了保证按钮大小即为内容大小,这里直接去掉,改为一个最小的值。
self.contentEdgeInsets = UIEdgeInsetsMake(CGFLOAT_MIN, 0, CGFLOAT_MIN, 0);
+
+ // 放在后面,让前面的默认值可以被子类重写的 didInitialize 覆盖
+ [self didInitialize];
}
return self;
}
@@ -50,9 +54,6 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {
}
- (void)didInitialize {
- self.adjustsTitleTintColorAutomatically = NO;
- self.adjustsImageTintColorAutomatically = NO;
-
// 默认接管highlighted和disabled的表现,去掉系统默认的表现
self.adjustsImageWhenHighlighted = NO;
self.adjustsImageWhenDisabled = NO;
@@ -61,73 +62,153 @@ - (void)didInitialize {
// 图片默认在按钮左边,与系统UIButton保持一致
self.imagePosition = QMUIButtonImagePositionLeft;
+
+ _qmuisubtitleLabel = [[UILabel alloc] init];
+ _qmuisubtitleLabel.textColor = self.subtitleColor;
+ _qmuisubtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
+
+ self.subtitleEdgeInsets = UIEdgeInsetsMake(4, 0, 0, 0);
}
-- (CGSize)sizeThatFits:(CGSize)size {
- // 如果调用 sizeToFit,那么传进来的 size 就是当前按钮的 size,此时的计算不要去限制宽高
- if (CGSizeEqualToSize(self.bounds.size, size)) {
- size = CGSizeMax;
+- (void)setSubtitle:(NSString *)subtitle {
+ _subtitle = subtitle;
+ if (subtitle.length) {
+ [self addSubview:_qmuisubtitleLabel];
+ _qmuisubtitleLabel.text = subtitle;
+ } else {
+ [_qmuisubtitleLabel removeFromSuperview];
}
+ [self setNeedsLayout];
+}
+
+- (void)setSubtitleEdgeInsets:(UIEdgeInsets)subtitleEdgeInsets {
+ _subtitleEdgeInsets = subtitleEdgeInsets;
+ [self setNeedsLayout];
+}
+
+- (void)setSubtitleColor:(UIColor *)subtitleColor {
+ _subtitleColor = subtitleColor;
+ _qmuisubtitleLabel.textColor = subtitleColor;
+}
+
+// 系统访问 self.imageView 会触发 layout,而私有方法 _imageView 则是简单地访问 imageView,所以在 QMUIButton layoutSubviews 里应该用这个方法
+// https://github.com/Tencent/QMUI_iOS/issues/1051
+- (UIImageView *)_qmui_imageView {
+ BeginIgnorePerformSelectorLeaksWarning
+ return [self performSelector:NSSelectorFromString(@"_imageView")];
+ EndIgnorePerformSelectorLeaksWarning
+}
+
+- (QMUILayouterItem *)generateLayouterForLayout:(BOOL)forLayout {
+ __weak __typeof(self)weakSelf = self;
+
+ QMUILayouterAlignment horizontal = [@[
+ @(QMUILayouterAlignmentCenter),
+ @(QMUILayouterAlignmentLeading),
+ @(QMUILayouterAlignmentTrailing),
+ @(QMUILayouterAlignmentFill),
+ @(QMUILayouterAlignmentLeading),
+ @(QMUILayouterAlignmentTrailing),
+ ][self.contentHorizontalAlignment] integerValue];
+ QMUILayouterAlignment vertical = [@[
+ @(QMUILayouterAlignmentCenter),
+ @(QMUILayouterAlignmentLeading),
+ @(QMUILayouterAlignmentTrailing),
+ @(QMUILayouterAlignmentFill),
+ ][self.contentVerticalAlignment] integerValue];
BOOL isImageViewShowing = !!self.currentImage;
- BOOL isTitleLabelShowing = !!self.currentTitle || self.currentAttributedTitle;
- CGSize imageTotalSize = CGSizeZero;// 包含 imageEdgeInsets 那些空间
- CGSize titleTotalSize = CGSizeZero;// 包含 titleEdgeInsets 那些空间
- CGFloat spacingBetweenImageAndTitle = flat(isImageViewShowing && isTitleLabelShowing ? self.spacingBetweenImageAndTitle : 0);// 如果图片或文字某一者没显示,则这个 spacing 不考虑进布局
- UIEdgeInsets contentEdgeInsets = UIEdgeInsetsRemoveFloatMin(self.contentEdgeInsets);
- CGSize resultSize = CGSizeZero;
- CGSize contentLimitSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(contentEdgeInsets), size.height - UIEdgeInsetsGetVerticalValue(contentEdgeInsets));
+ QMUILayouterItem *image = [QMUILayouterItem itemWithView:isImageViewShowing ? (forLayout ? self._qmui_imageView : self.imageView) : nil margin:self.imageEdgeInsets];
+ image.visibleBlock = ^BOOL(QMUILayouterItem * _Nonnull aItem) {
+ return !!weakSelf.currentImage;
+ };
+ image.sizeThatFitsBlock = ^CGSize(QMUILayouterItem * _Nonnull aItem, CGSize size, CGSize superResult) {
+ // 某些时机下存在 image 但 imageView.image 尚为 nil 导致计算出来的尺寸错误,所以这里做个保护(ed4d87e86af12110b2c14359ef287be959c70af0)
+ if (aItem.visible && CGSizeIsEmpty(superResult) && [aItem.view.superview isKindOfClass:QMUIButton.class]) {
+ QMUIButton *btn = (QMUIButton *)aItem.view.superview;
+ return btn.currentImage.size;
+ }
+ return superResult;
+ };
+ QMUILayouterItem *title = [QMUILayouterItem itemWithView:self.titleLabel margin:self.titleEdgeInsets grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkDefault];
+ title.visibleBlock = ^BOOL(QMUILayouterItem * _Nonnull aItem) {
+ return !!weakSelf.currentTitle || !!weakSelf.currentAttributedTitle;
+ };
+ QMUILayouterItem *subtitle = [QMUILayouterItem itemWithView:self.subtitleLabel margin:self.subtitleEdgeInsets grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkDefault];
+ QMUILayouterLinearVertical *titles = [QMUILayouterLinearVertical itemWithChildItems:@[
+ title,
+ subtitle,
+ ] spacingBetweenItems:0 horizontal:horizontal vertical:vertical];
+ titles.shrink = QMUILayouterShrinkDefault;
- switch (self.imagePosition) {
- case QMUIButtonImagePositionTop:
- case QMUIButtonImagePositionBottom: {
- // 图片和文字上下排版时,宽度以文字或图片的最大宽度为最终宽度
- if (isImageViewShowing) {
- CGFloat imageLimitWidth = contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets);
- CGSize imageSize = self.imageView.image ? [self.imageView sizeThatFits:CGSizeMake(imageLimitWidth, CGFLOAT_MAX)] : self.currentImage.size;
- imageSize.width = fmin(imageSize.width, imageLimitWidth);
- imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
- }
-
- if (isTitleLabelShowing) {
- CGSize titleLimitSize = CGSizeMake(contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), contentLimitSize.height - imageTotalSize.height - spacingBetweenImageAndTitle - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize];
- titleSize.height = fmin(titleSize.height, titleLimitSize.height);
- titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
+ if (self.imagePosition == QMUIButtonImagePositionTop || self.imagePosition == QMUIButtonImagePositionBottom) {
+ if (vertical == QMUILayouterAlignmentFill) {
+ if (image.visible && title.visible && !subtitle.visible) {
+ titles.grow = QMUILayouterGrowMost;
+ title.grow = QMUILayouterGrowMost;
+ } else if (image.visible && !title.visible && subtitle.visible) {
+ titles.grow = QMUILayouterGrowMost;
+ subtitle.grow = QMUILayouterGrowMost;
+ } else if (!image.visible && title.visible && subtitle.visible) {
+ titles.grow = QMUILayouterGrowMost;
+ title.grow = QMUILayouterGrowMost;
}
-
- resultSize.width = UIEdgeInsetsGetHorizontalValue(contentEdgeInsets);
- resultSize.width += fmax(imageTotalSize.width, titleTotalSize.width);
- resultSize.height = UIEdgeInsetsGetVerticalValue(contentEdgeInsets) + imageTotalSize.height + spacingBetweenImageAndTitle + titleTotalSize.height;
}
- break;
-
- case QMUIButtonImagePositionLeft:
- case QMUIButtonImagePositionRight: {
- // 图片和文字水平排版时,高度以文字或图片的最大高度为最终高度
- // 注意这里有一个和系统不一致的行为:当 titleLabel 为多行时,系统的 sizeThatFits: 计算结果固定是单行的,所以当 QMUIButtonImagePositionLeft 并且titleLabel 多行的情况下,QMUIButton 计算的结果与系统不一致
-
- if (isImageViewShowing) {
- CGFloat imageLimitHeight = contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets);
- CGSize imageSize = self.imageView.image ? [self.imageView sizeThatFits:CGSizeMake(CGFLOAT_MAX, imageLimitHeight)] : self.currentImage.size;
- imageSize.height = fmin(imageSize.height, imageLimitHeight);
- imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
+ } else if (self.imagePosition == QMUIButtonImagePositionLeft || self.imagePosition == QMUIButtonImagePositionRight) {
+ if (horizontal == QMUILayouterAlignmentFill) {
+ if (image.visible && (title.visible || subtitle.visible)) {
+ titles.grow = QMUILayouterGrowMost;
}
-
- if (isTitleLabelShowing) {
- CGSize titleLimitSize = CGSizeMake(contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) - imageTotalSize.width - spacingBetweenImageAndTitle, contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize];
- titleSize.height = fmin(titleSize.height, titleLimitSize.height);
- titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
+ }
+ if (vertical == QMUILayouterAlignmentFill) {
+ if (title.visible) {
+ title.grow = QMUILayouterGrowMost;
+ } else if (subtitle.visible) {
+ subtitle.grow = QMUILayouterGrowMost;
}
-
- resultSize.width = UIEdgeInsetsGetHorizontalValue(contentEdgeInsets) + imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width;
- resultSize.height = UIEdgeInsetsGetVerticalValue(contentEdgeInsets);
- resultSize.height += fmax(imageTotalSize.height, titleTotalSize.height);
}
- break;
}
- return resultSize;
+
+ switch (self.imagePosition) {
+ case QMUIButtonImagePositionTop: {
+ return [QMUILayouterLinearVertical itemWithChildItems:@[
+ image,
+ titles,
+ ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical];
+ }
+ case QMUIButtonImagePositionBottom: {
+ return [QMUILayouterLinearVertical itemWithChildItems:@[
+ titles,
+ image,
+ ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical];
+ }
+ case QMUIButtonImagePositionLeft: {
+ return [QMUILayouterLinearHorizontal itemWithChildItems:@[
+ image,
+ titles,
+ ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical];
+ }
+ case QMUIButtonImagePositionRight: {
+ return [QMUILayouterLinearHorizontal itemWithChildItems:@[
+ titles,
+ image,
+ ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical];
+ }
+ }
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+ // 如果调用 sizeToFit,那么传进来的 size 就是当前按钮的 size,此时的计算不要去限制宽高
+ // 系统 UIButton 不管任何时候,对 sizeThatFits:CGSizeZero 都会返回真实的内容大小,这里对齐
+ if (CGSizeEqualToSize(self.bounds.size, size) || CGSizeIsEmpty(size)) {
+ size = CGSizeMax;
+ }
+
+ QMUILayouterItem *layouter = [self generateLayouterForLayout:NO];
+ CGSize result = [layouter sizeThatFits:size];
+ result.width += UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets);
+ result.height += UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets);
+ return result;
}
- (CGSize)intrinsicContentSize {
@@ -141,277 +222,29 @@ - (void)layoutSubviews {
return;
}
- BOOL isImageViewShowing = !!self.currentImage;
- BOOL isTitleLabelShowing = !!self.currentTitle || !!self.currentAttributedTitle;
- CGSize imageLimitSize = CGSizeZero;
- CGSize titleLimitSize = CGSizeZero;
- CGSize imageTotalSize = CGSizeZero;// 包含 imageEdgeInsets 那些空间
- CGSize titleTotalSize = CGSizeZero;// 包含 titleEdgeInsets 那些空间
- CGFloat spacingBetweenImageAndTitle = flat(isImageViewShowing && isTitleLabelShowing ? self.spacingBetweenImageAndTitle : 0);// 如果图片或文字某一者没显示,则这个 spacing 不考虑进布局
- CGRect imageFrame = CGRectZero;
- CGRect titleFrame = CGRectZero;
- UIEdgeInsets contentEdgeInsets = UIEdgeInsetsRemoveFloatMin(self.contentEdgeInsets);
- CGSize contentSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(contentEdgeInsets), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(contentEdgeInsets));
-
- // 图片的布局原则都是尽量完整展示,所以不管 imagePosition 的值是什么,这个计算过程都是相同的
- if (isImageViewShowing) {
- imageLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
- CGSize imageSize = self.imageView.image ? [self.imageView sizeThatFits:imageLimitSize] : self.currentImage.size;
- imageSize.width = fmin(imageLimitSize.width, imageSize.width);
- imageSize.height = fmin(imageLimitSize.height, imageSize.height);
- imageFrame = CGRectMakeWithSize(imageSize);
- imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
+ if (self.cornerRadius == QMUIButtonCornerRadiusAdjustsBounds) {
+ self.layer.cornerRadius = CGRectGetHeight(self.bounds) / 2;
}
- if (self.imagePosition == QMUIButtonImagePositionTop || self.imagePosition == QMUIButtonImagePositionBottom) {
-
- if (isTitleLabelShowing) {
- titleLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), contentSize.height - imageTotalSize.height - spacingBetweenImageAndTitle - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize];
- titleSize.width = fmin(titleLimitSize.width, titleSize.width);
- titleSize.height = fmin(titleLimitSize.height, titleSize.height);
- titleFrame = CGRectMakeWithSize(titleSize);
- titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- }
-
- switch (self.contentHorizontalAlignment) {
- case UIControlContentHorizontalAlignmentLeft:
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left) : titleFrame;
- break;
- case UIControlContentHorizontalAlignmentCenter:
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left + CGFloatGetCenter(imageLimitSize.width, CGRectGetWidth(imageFrame))) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left + CGFloatGetCenter(titleLimitSize.width, CGRectGetWidth(titleFrame))) : titleFrame;
- break;
- case UIControlContentHorizontalAlignmentRight:
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame;
- break;
- case UIControlContentHorizontalAlignmentFill:
- if (isImageViewShowing) {
- imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left);
- imageFrame = CGRectSetWidth(imageFrame, imageLimitSize.width);
- }
- if (isTitleLabelShowing) {
- titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left);
- titleFrame = CGRectSetWidth(titleFrame, titleLimitSize.width);
- }
- break;
- default:
- break;
- }
-
- if (self.imagePosition == QMUIButtonImagePositionTop) {
- switch (self.contentVerticalAlignment) {
- case UIControlContentVerticalAlignmentTop:
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame;
- break;
- case UIControlContentVerticalAlignmentCenter: {
- CGFloat contentHeight = imageTotalSize.height + spacingBetweenImageAndTitle + titleTotalSize.height;
- CGFloat minY = CGFloatGetCenter(contentSize.height, contentHeight) + contentEdgeInsets.top;
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, minY + self.imageEdgeInsets.top) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, minY + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame;
- }
- break;
- case UIControlContentVerticalAlignmentBottom:
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame;
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - titleTotalSize.height - spacingBetweenImageAndTitle - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame;
- break;
- case UIControlContentVerticalAlignmentFill: {
- if (isImageViewShowing && isTitleLabelShowing) {
-
- // 同时显示图片和 label 的情况下,图片高度按本身大小显示,剩余空间留给 label
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetHeight(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetMinY(titleFrame)) : titleFrame;
-
- } else if (isImageViewShowing) {
- imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top);
- imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
- } else {
- titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top);
- titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- }
- }
- break;
- }
- } else {
- switch (self.contentVerticalAlignment) {
- case UIControlContentVerticalAlignmentTop:
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top) : titleFrame;
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + titleTotalSize.height + spacingBetweenImageAndTitle + self.imageEdgeInsets.top) : imageFrame;
- break;
- case UIControlContentVerticalAlignmentCenter: {
- CGFloat contentHeight = imageTotalSize.height + titleTotalSize.height + spacingBetweenImageAndTitle;
- CGFloat minY = CGFloatGetCenter(contentSize.height, contentHeight) + contentEdgeInsets.top;
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, minY + self.titleEdgeInsets.top) : titleFrame;
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, minY + titleTotalSize.height + spacingBetweenImageAndTitle + self.imageEdgeInsets.top) : imageFrame;
- }
- break;
- case UIControlContentVerticalAlignmentBottom:
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - imageTotalSize.height - spacingBetweenImageAndTitle - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame;
- break;
- case UIControlContentVerticalAlignmentFill: {
- if (isImageViewShowing && isTitleLabelShowing) {
-
- // 同时显示图片和 label 的情况下,图片高度按本身大小显示,剩余空间留给 label
- imageFrame = CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame));
- titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top);
- titleFrame = CGRectSetHeight(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - imageTotalSize.height - spacingBetweenImageAndTitle - self.titleEdgeInsets.bottom - CGRectGetMinY(titleFrame));
-
- } else if (isImageViewShowing) {
- imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top);
- imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
- } else {
- titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top);
- titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- }
- }
- break;
- }
- }
-
- if (isImageViewShowing) {
- self.imageView.frame = CGRectFlatted(imageFrame);
- }
- if (isTitleLabelShowing) {
- self.titleLabel.frame = CGRectFlatted(titleFrame);
- }
-
- } else if (self.imagePosition == QMUIButtonImagePositionLeft || self.imagePosition == QMUIButtonImagePositionRight) {
-
- if (isTitleLabelShowing) {
- titleLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) - imageTotalSize.width - spacingBetweenImageAndTitle, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize];
- titleSize.width = fmin(titleLimitSize.width, titleSize.width);
- titleSize.height = fmin(titleLimitSize.height, titleSize.height);
- titleFrame = CGRectMakeWithSize(titleSize);
- titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- }
-
- switch (self.contentVerticalAlignment) {
- case UIControlContentVerticalAlignmentTop:
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top) : titleFrame;
-
- break;
- case UIControlContentVerticalAlignmentCenter:
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + CGFloatGetCenter(contentSize.height, CGRectGetHeight(imageFrame)) + self.imageEdgeInsets.top) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + CGFloatGetCenter(contentSize.height, CGRectGetHeight(titleFrame)) + self.titleEdgeInsets.top) : titleFrame;
- break;
- case UIControlContentVerticalAlignmentBottom:
- imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame;
- break;
- case UIControlContentVerticalAlignmentFill:
- if (isImageViewShowing) {
- imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top);
- imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
- }
- if (isTitleLabelShowing) {
- titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top);
- titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
- }
- break;
- }
-
- if (self.imagePosition == QMUIButtonImagePositionLeft) {
- switch (self.contentHorizontalAlignment) {
- case UIControlContentHorizontalAlignmentLeft:
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame;
- break;
- case UIControlContentHorizontalAlignmentCenter: {
- CGFloat contentWidth = imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width;
- CGFloat minX = contentEdgeInsets.left + CGFloatGetCenter(contentSize.width, contentWidth);
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, minX + self.imageEdgeInsets.left) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, minX + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame;
- }
- break;
- case UIControlContentHorizontalAlignmentRight: {
- if (imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width > contentSize.width) {
- // 图片和文字总宽超过按钮宽度,则优先完整显示图片
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame;
- } else {
- // 内容不超过按钮宽度,则靠右布局即可
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame;
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - titleTotalSize.width - spacingBetweenImageAndTitle - imageTotalSize.width + self.imageEdgeInsets.left) : imageFrame;
- }
- }
- break;
- case UIControlContentHorizontalAlignmentFill: {
- if (isImageViewShowing && isTitleLabelShowing) {
- // 同时显示图片和 label 的情况下,图片按本身宽度显示,剩余空间留给 label
- imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left);
- titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left);
- titleFrame = CGRectSetWidth(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame));
- } else if (isImageViewShowing) {
- imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left);
- imageFrame = CGRectSetWidth(imageFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets));
- } else {
- titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left);
- titleFrame = CGRectSetWidth(titleFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets));
- }
- }
- break;
- default:
- break;
- }
- } else {
- switch (self.contentHorizontalAlignment) {
- case UIControlContentHorizontalAlignmentLeft: {
- if (imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width > contentSize.width) {
- // 图片和文字总宽超过按钮宽度,则优先完整显示图片
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - imageTotalSize.width - spacingBetweenImageAndTitle - titleTotalSize.width + self.titleEdgeInsets.left) : titleFrame;
- } else {
- // 内容不超过按钮宽度,则靠左布局即可
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left) : titleFrame;
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + titleTotalSize.width + spacingBetweenImageAndTitle + self.imageEdgeInsets.left) : imageFrame;
- }
- }
- break;
- case UIControlContentHorizontalAlignmentCenter: {
- CGFloat contentWidth = imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width;
- CGFloat minX = contentEdgeInsets.left + CGFloatGetCenter(contentSize.width, contentWidth);
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, minX + self.titleEdgeInsets.left) : titleFrame;
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, minX + titleTotalSize.width + spacingBetweenImageAndTitle + self.imageEdgeInsets.left) : imageFrame;
- }
- break;
- case UIControlContentHorizontalAlignmentRight:
- imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame;
- titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - imageTotalSize.width - spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame;
- break;
- case UIControlContentHorizontalAlignmentFill: {
- if (isImageViewShowing && isTitleLabelShowing) {
- // 图片按自身大小显示,剩余空间由标题占满
- imageFrame = CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame));
- titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left);
- titleFrame = CGRectSetWidth(titleFrame, CGRectGetMinX(imageFrame) - self.imageEdgeInsets.left - spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame));
-
- } else if (isImageViewShowing) {
- imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left);
- imageFrame = CGRectSetWidth(imageFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets));
- } else {
- titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left);
- titleFrame = CGRectSetWidth(titleFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets));
- }
- }
- break;
- default:
- break;
- }
- }
-
- if (isImageViewShowing) {
- self.imageView.frame = CGRectFlatted(imageFrame);
+ QMUILayouterItem *layouter = [self generateLayouterForLayout:YES];
+ layouter.frame = CGRectInsetEdges(self.bounds, self.contentEdgeInsets);
+ [layouter layoutIfNeeded];
+
+ // UIButton 有一个特性是不管哪种 alignment,imageView 的宽高必定不超过 button 的宽高(也不管 imageView 的宽高比例是否产生变化),从而保证就算设置了超过 button 大小的 image,也会在 button 容器内部显示。这里对齐系统的特性
+ BOOL isImageViewShowing = !!self.currentImage;
+ if (isImageViewShowing && !CGRectIsEmpty(self.bounds)) {
+ UIImageView *imageView = self._qmui_imageView;
+ CGRect rect = imageView.frame;
+ CGRect limitRect = CGRectInsetEdges(CGRectInsetEdges(self.bounds, self.contentEdgeInsets), self.imageEdgeInsets);
+ if (CGRectGetWidth(rect) > CGRectGetWidth(limitRect)) {
+ rect = CGRectSetWidth(rect, CGRectGetWidth(limitRect));
+ rect = CGRectSetX(rect, self.contentEdgeInsets.left + self.imageEdgeInsets.left);
}
- if (isTitleLabelShowing) {
- self.titleLabel.frame = CGRectFlatted(titleFrame);
+ if (CGRectGetHeight(rect) > CGRectGetHeight(limitRect)) {
+ rect = CGRectSetHeight(rect, CGRectGetHeight(limitRect));
+ rect = CGRectSetY(rect, self.contentEdgeInsets.top + self.imageEdgeInsets.top);
}
+ imageView.frame = rect;
}
}
@@ -471,10 +304,8 @@ - (void)setHighlighted:(BOOL)highlighted {
- (void)setEnabled:(BOOL)enabled {
[super setEnabled:enabled];
- if (!enabled && self.adjustsButtonWhenDisabled) {
- self.alpha = ButtonDisabledAlpha;
- } else {
- self.alpha = 1;
+ if (self.adjustsButtonWhenDisabled) {
+ self.alpha = enabled ? 1 : ButtonDisabledAlpha;
}
}
@@ -487,6 +318,7 @@ - (void)adjustsButtonHighlighted {
}
self.highlightedBackgroundLayer.frame = self.bounds;
self.highlightedBackgroundLayer.cornerRadius = self.layer.cornerRadius;
+ self.highlightedBackgroundLayer.maskedCorners = self.layer.maskedCorners;
self.highlightedBackgroundLayer.backgroundColor = self.highlighted ? self.highlightedBackgroundColor.CGColor : UIColorClear.CGColor;
}
@@ -501,14 +333,16 @@ - (void)setAdjustsTitleTintColorAutomatically:(BOOL)adjustsTitleTintColorAutomat
}
- (void)updateTitleColorIfNeeded {
- if (self.adjustsTitleTintColorAutomatically && self.currentTitleColor) {
+ if (!self.adjustsTitleTintColorAutomatically) return;
+ if (self.currentTitleColor) {
[self setTitleColor:self.tintColor forState:UIControlStateNormal];
}
- if (self.adjustsTitleTintColorAutomatically && self.currentAttributedTitle) {
+ if (self.currentAttributedTitle) {
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:self.currentAttributedTitle];
[attributedString addAttribute:NSForegroundColorAttributeName value:self.tintColor range:NSMakeRange(0, attributedString.length)];
[self setAttributedTitle:attributedString forState:UIControlStateNormal];
}
+ self.subtitleColor = self.tintColor;
}
- (void)setAdjustsImageTintColorAutomatically:(BOOL)adjustsImageTintColorAutomatically {
@@ -527,7 +361,10 @@ - (void)updateImageRenderingModeIfNeeded {
for (NSNumber *number in states) {
UIImage *image = [self imageForState:number.unsignedIntegerValue];
if (!image) {
- return;
+ continue;
+ }
+ if (number.unsignedIntegerValue != UIControlStateNormal && image == [self imageForState:UIControlStateNormal]) {
+ continue;
}
if (self.adjustsImageTintColorAutomatically) {
@@ -542,7 +379,7 @@ - (void)updateImageRenderingModeIfNeeded {
}
- (void)setImage:(UIImage *)image forState:(UIControlState)state {
- if (self.adjustsImageTintColorAutomatically) {
+ if (self.adjustsImageTintColorAutomatically && image.renderingMode != UIImageRenderingModeAlwaysOriginal) {
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
@@ -560,6 +397,7 @@ - (void)tintColorDidChange {
}
- (void)setTintColorAdjustsTitleAndImage:(UIColor *)tintColorAdjustsTitleAndImage {
+ _tintColorAdjustsTitleAndImage = tintColorAdjustsTitleAndImage;
if (tintColorAdjustsTitleAndImage) {
self.tintColor = tintColorAdjustsTitleAndImage;
self.adjustsTitleTintColorAutomatically = YES;
@@ -567,8 +405,12 @@ - (void)setTintColorAdjustsTitleAndImage:(UIColor *)tintColorAdjustsTitleAndImag
}
}
-- (UIColor *)tintColorAdjustsTitleAndImage {
- return self.tintColor;
+- (void)setCornerRadius:(CGFloat)cornerRadius {
+ _cornerRadius = cornerRadius;
+ if (cornerRadius != QMUIButtonCornerRadiusAdjustsBounds) {
+ self.layer.cornerRadius = cornerRadius;
+ }
+ [self setNeedsLayout];
}
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.h
deleted file mode 100644
index f6b84432..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUIFillButton.h
-// QMUIKit
-//
-// Created by QMUI Team on 2018/4/9.
-//
-
-#import "QMUIButton.h"
-
-NS_ASSUME_NONNULL_BEGIN
-
-typedef NS_ENUM(NSUInteger, QMUIFillButtonColor) {
- QMUIFillButtonColorBlue,
- QMUIFillButtonColorRed,
- QMUIFillButtonColorGreen,
- QMUIFillButtonColorGray,
- QMUIFillButtonColorWhite,
-};
-
-/**
- * 用于 `QMUIFillButton.cornerRadius` 属性,当 `cornerRadius` 为 `QMUIFillButtonCornerRadiusAdjustsBounds` 时,`QMUIFillButton` 会在高度变化时自动调整 `cornerRadius`,使其始终保持为高度的 1/2。
- */
-extern const CGFloat QMUIFillButtonCornerRadiusAdjustsBounds;
-
-/**
- * QMUIFillButton
- * 实心填充颜色的按钮,支持预定义的几个色值
- */
-@interface QMUIFillButton : QMUIButton
-
-@property(nonatomic, strong, nullable) IBInspectable UIColor *fillColor; // 默认为 FillButtonColorBlue
-@property(nonatomic, strong, nullable) IBInspectable UIColor *titleTextColor; // 默认为 UIColorWhite
-@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR;// 默认为 QMUIFillButtonCornerRadiusAdjustsBounds,也即固定保持按钮高度的一半。
-
-/**
- * 控制按钮里面的图片是否也要跟随 `titleTextColor` 一起变化,默认为 `NO`
- */
-@property(nonatomic, assign) BOOL adjustsImageWithTitleTextColor UI_APPEARANCE_SELECTOR;
-
-- (instancetype)initWithFillType:(QMUIFillButtonColor)fillType;
-- (instancetype)initWithFillType:(QMUIFillButtonColor)fillType frame:(CGRect)frame;
-- (instancetype)initWithFillColor:(nullable UIColor *)fillColor titleTextColor:(nullable UIColor *)textColor;
-- (instancetype)initWithFillColor:(nullable UIColor *)fillColor titleTextColor:(nullable UIColor *)textColor frame:(CGRect)frame;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.m
deleted file mode 100644
index 83999c21..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.m
+++ /dev/null
@@ -1,163 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUIFillButton.m
-// QMUIKit
-//
-// Created by QMUI Team on 2018/4/9.
-//
-
-#import "QMUIFillButton.h"
-#import "QMUICore.h"
-
-const CGFloat QMUIFillButtonCornerRadiusAdjustsBounds = -1;
-
-@implementation QMUIFillButton
-
-- (instancetype)init {
- return [self initWithFillType:QMUIFillButtonColorBlue];
-}
-
-- (instancetype)initWithFrame:(CGRect)frame {
- return [self initWithFillType:QMUIFillButtonColorBlue frame:frame];
-}
-
-- (instancetype)initWithFillType:(QMUIFillButtonColor)fillType {
- return [self initWithFillType:fillType frame:CGRectZero];
-}
-
-- (instancetype)initWithFillType:(QMUIFillButtonColor)fillType frame:(CGRect)frame {
- UIColor *fillColor = nil;
- UIColor *textColor = UIColorWhite;
- switch (fillType) {
- case QMUIFillButtonColorBlue:
- fillColor = FillButtonColorBlue;
- break;
- case QMUIFillButtonColorRed:
- fillColor = FillButtonColorRed;
- break;
- case QMUIFillButtonColorGreen:
- fillColor = FillButtonColorGreen;
- break;
- case QMUIFillButtonColorGray:
- fillColor = FillButtonColorGray;
- break;
- case QMUIFillButtonColorWhite:
- fillColor = FillButtonColorWhite;
- textColor = UIColorBlue;
- default:
- break;
- }
- return [self initWithFillColor:fillColor titleTextColor:textColor frame:frame];
-}
-
-- (instancetype)initWithFillColor:(UIColor *)fillColor titleTextColor:(UIColor *)textColor {
- return [self initWithFillColor:fillColor titleTextColor:textColor frame:CGRectZero];
-}
-
-- (instancetype)initWithFillColor:(UIColor *)fillColor titleTextColor:(UIColor *)textColor frame:(CGRect)frame {
- self = [super initWithFrame:frame];
- if (self) {
- self.fillColor = fillColor;
- self.titleTextColor = textColor;
- }
- return self;
-}
-
-- (instancetype)initWithCoder:(NSCoder *)aDecoder {
- if (self = [super initWithCoder:aDecoder]) {
- self.fillColor = FillButtonColorBlue;
- self.titleTextColor = UIColorWhite;
- }
- return self;
-}
-
-- (void)setAdjustsImageWithTitleTextColor:(BOOL)adjustsImageWithTitleTextColor {
- _adjustsImageWithTitleTextColor = adjustsImageWithTitleTextColor;
- if (adjustsImageWithTitleTextColor) {
- [self updateImageColor];
- }
-}
-
-- (void)setFillColor:(UIColor *)fillColor {
- _fillColor = fillColor;
- self.backgroundColor = fillColor;
-}
-
-- (void)setTitleTextColor:(UIColor *)titleTextColor {
- _titleTextColor = titleTextColor;
- [self setTitleColor:titleTextColor forState:UIControlStateNormal];
- if (self.adjustsImageWithTitleTextColor) {
- [self updateImageColor];
- }
-}
-
-- (void)setImage:(UIImage *)image forState:(UIControlState)state {
- if (self.adjustsImageWithTitleTextColor) {
- image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
- }
- [super setImage:image forState:state];
-}
-
-- (void)updateImageColor {
- self.imageView.tintColor = self.adjustsImageWithTitleTextColor ? self.titleTextColor : nil;
- if (self.currentImage) {
- NSArray *states = @[@(UIControlStateNormal), @(UIControlStateHighlighted), @(UIControlStateDisabled)];
- for (NSNumber *number in states) {
- UIImage *image = [self imageForState:[number unsignedIntegerValue]];
- if (!image) {
- continue;
- }
- if (self.adjustsImageWithTitleTextColor) {
- // 这里的image不用做renderingMode的处理,而是放到重写的setImage:forState里去做
- [self setImage:image forState:[number unsignedIntegerValue]];
- } else {
- // 如果不需要用template的模式渲染,并且之前是使用template的,则把renderingMode改回Original
- [self setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:[number unsignedIntegerValue]];
- }
- }
- }
-}
-
-- (void)layoutSublayersOfLayer:(CALayer *)layer {
- [super layoutSublayersOfLayer:layer];
- if (self.cornerRadius != QMUIFillButtonCornerRadiusAdjustsBounds) {
- self.layer.cornerRadius = self.cornerRadius;
- } else {
- self.layer.cornerRadius = CGRectGetHeight(self.bounds) / 2;
- }
-}
-
-- (void)setCornerRadius:(CGFloat)cornerRadius {
- _cornerRadius = cornerRadius;
- [self setNeedsLayout];
-}
-
-@end
-
-@interface QMUIFillButton (UIAppearance)
-
-@end
-
-@implementation QMUIFillButton (UIAppearance)
-
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self setDefaultAppearance];
- });
-}
-
-+ (void)setDefaultAppearance {
- QMUIFillButton *appearance = [QMUIFillButton appearance];
- appearance.cornerRadius = QMUIFillButtonCornerRadiusAdjustsBounds;
- appearance.adjustsImageWithTitleTextColor = NO;
-}
-
-@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.h
deleted file mode 100644
index 8b6df268..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUIGhostButton.h
-// QMUIKit
-//
-// Created by QMUI Team on 2018/4/9.
-//
-
-#import "QMUIButton.h"
-
-NS_ASSUME_NONNULL_BEGIN
-
-typedef NS_ENUM(NSUInteger, QMUIGhostButtonColor) {
- QMUIGhostButtonColorBlue,
- QMUIGhostButtonColorRed,
- QMUIGhostButtonColorGreen,
- QMUIGhostButtonColorGray,
- QMUIGhostButtonColorWhite,
-};
-
-/**
- * 用于 `QMUIGhostButton.cornerRadius` 属性,当 `cornerRadius` 为 `QMUIGhostButtonCornerRadiusAdjustsBounds` 时,`QMUIGhostButton` 会在高度变化时自动调整 `cornerRadius`,使其始终保持为高度的 1/2。
- */
-extern const CGFloat QMUIGhostButtonCornerRadiusAdjustsBounds;
-
-/**
- * “幽灵”按钮,也即背景透明、带圆角边框的按钮
- *
- * 可通过 `QMUIGhostButtonColor` 设置几种预设的颜色,也可以用 `ghostColor` 设置自定义颜色。
- *
- * @warning 默认情况下,`ghostColor` 只会修改文字和边框的颜色,如果需要让 image 也跟随 `ghostColor` 的颜色,则可将 `adjustsImageWithGhostColor` 设为 `YES`
- */
-@interface QMUIGhostButton : QMUIButton
-
-@property(nonatomic, strong, nullable) IBInspectable UIColor *ghostColor; // 默认为 GhostButtonColorBlue
-@property(nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR; // 默认为 1pt
-@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; // 默认为 QMUIGhostButtonCornerRadiusAdjustsBounds,也即固定保持按钮高度的一半。
-
-/**
- * 控制按钮里面的图片是否也要跟随 `ghostColor` 一起变化,默认为 `NO`
- */
-@property(nonatomic, assign) BOOL adjustsImageWithGhostColor UI_APPEARANCE_SELECTOR;
-
-- (instancetype)initWithGhostType:(QMUIGhostButtonColor)ghostType;
-- (instancetype)initWithGhostColor:(nullable UIColor *)ghostColor;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.m
deleted file mode 100644
index a4dc619a..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.m
+++ /dev/null
@@ -1,160 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUIGhostButton.m
-// QMUIKit
-//
-// Created by QMUI Team on 2018/4/9.
-//
-
-#import "QMUIGhostButton.h"
-#import "QMUICore.h"
-
-const CGFloat QMUIGhostButtonCornerRadiusAdjustsBounds = -1;
-
-@implementation QMUIGhostButton
-
-- (instancetype)initWithFrame:(CGRect)frame {
- return [self initWithGhostType:QMUIGhostButtonColorBlue frame:frame];
-}
-
-- (instancetype)initWithGhostType:(QMUIGhostButtonColor)ghostType {
- return [self initWithGhostType:ghostType frame:CGRectZero];
-}
-
-- (instancetype)initWithGhostType:(QMUIGhostButtonColor)ghostType frame:(CGRect)frame {
- UIColor *ghostColor = nil;
- switch (ghostType) {
- case QMUIGhostButtonColorBlue:
- ghostColor = GhostButtonColorBlue;
- break;
- case QMUIGhostButtonColorRed:
- ghostColor = GhostButtonColorRed;
- break;
- case QMUIGhostButtonColorGreen:
- ghostColor = GhostButtonColorGreen;
- break;
- case QMUIGhostButtonColorGray:
- ghostColor = GhostButtonColorGray;
- break;
- case QMUIGhostButtonColorWhite:
- ghostColor = GhostButtonColorWhite;
- break;
- default:
- break;
- }
- return [self initWithGhostColor:ghostColor frame:frame];
-}
-
-- (instancetype)initWithGhostColor:(UIColor *)ghostColor {
- return [self initWithGhostColor:ghostColor frame:CGRectZero];
-}
-
-- (instancetype)initWithGhostColor:(UIColor *)ghostColor frame:(CGRect)frame {
- self = [super initWithFrame:frame];
- if (self) {
- [self initializeWithGhostColor:ghostColor];
- }
- return self;
-}
-
-- (instancetype)initWithCoder:(NSCoder *)aDecoder {
- if (self = [super initWithCoder:aDecoder]) {
- [self initializeWithGhostColor:GhostButtonColorBlue];
- }
- return self;
-}
-
-- (void)initializeWithGhostColor:(UIColor *)ghostColor {
- self.ghostColor = ghostColor;
-}
-
-- (void)setGhostColor:(UIColor *)ghostColor {
- _ghostColor = ghostColor;
- [self setTitleColor:_ghostColor forState:UIControlStateNormal];
- self.layer.borderColor = _ghostColor.CGColor;
- if (self.adjustsImageWithGhostColor) {
- [self updateImageColor];
- }
-}
-
-- (void)setBorderWidth:(CGFloat)borderWidth {
- _borderWidth = borderWidth;
- self.layer.borderWidth = _borderWidth;
-}
-
-- (void)setAdjustsImageWithGhostColor:(BOOL)adjustsImageWithGhostColor {
- _adjustsImageWithGhostColor = adjustsImageWithGhostColor;
- [self updateImageColor];
-}
-
-- (void)updateImageColor {
- self.imageView.tintColor = self.adjustsImageWithGhostColor ? self.ghostColor : nil;
- if (self.currentImage) {
- NSArray *states = @[@(UIControlStateNormal), @(UIControlStateHighlighted), @(UIControlStateDisabled)];
- for (NSNumber *number in states) {
- UIImage *image = [self imageForState:[number unsignedIntegerValue]];
- if (!image) {
- continue;
- }
- if (self.adjustsImageWithGhostColor) {
- // 这里的image不用做renderingMode的处理,而是放到重写的setImage:forState里去做
- [self setImage:image forState:[number unsignedIntegerValue]];
- } else {
- // 如果不需要用template的模式渲染,并且之前是使用template的,则把renderingMode改回Original
- [self setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:[number unsignedIntegerValue]];
- }
- }
- }
-}
-
-- (void)setImage:(UIImage *)image forState:(UIControlState)state {
- if (self.adjustsImageWithGhostColor) {
- image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
- }
- [super setImage:image forState:state];
-}
-
-- (void)layoutSublayersOfLayer:(CALayer *)layer {
- [super layoutSublayersOfLayer:layer];
- if (self.cornerRadius != QMUIGhostButtonCornerRadiusAdjustsBounds) {
- self.layer.cornerRadius = self.cornerRadius;
- } else {
- self.layer.cornerRadius = CGRectGetHeight(self.bounds) / 2;
- }
-}
-
-- (void)setCornerRadius:(CGFloat)cornerRadius {
- _cornerRadius = cornerRadius;
- [self setNeedsLayout];
-}
-
-@end
-
-@interface QMUIGhostButton (UIAppearance)
-
-@end
-
-@implementation QMUIGhostButton (UIAppearance)
-
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self setDefaultAppearance];
- });
-}
-
-+ (void)setDefaultAppearance {
- QMUIGhostButton *appearance = [QMUIGhostButton appearance];
- appearance.borderWidth = 1;
- appearance.cornerRadius = QMUIGhostButtonCornerRadiusAdjustsBounds;
- appearance.adjustsImageWithGhostColor = NO;
-}
-
-@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.h
deleted file mode 100644
index 7aa32e87..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUILinkButton.h
-// QMUIKit
-//
-// Created by QMUI Team on 2018/4/9.
-//
-
-#import "QMUIButton.h"
-
-NS_ASSUME_NONNULL_BEGIN
-
-/**
- * 支持显示下划线的按钮,可用于需要链接的场景。下划线默认和按钮宽度一样,可通过 `underlineInsets` 调整。
- */
-@interface QMUILinkButton : QMUIButton
-
-/// 控制下划线隐藏或显示,默认为NO,也即显示下划线
-@property(nonatomic, assign) IBInspectable BOOL underlineHidden;
-
-/// 设置下划线的宽度,默认为 1
-@property(nonatomic, assign) IBInspectable CGFloat underlineWidth;
-
-/// 控制下划线颜色,若设置为nil,则使用当前按钮的titleColor的颜色作为下划线的颜色。默认为 nil。
-@property(nonatomic, strong, nullable) IBInspectable UIColor *underlineColor;
-
-/// 下划线的位置是基于 titleLabel 的位置来计算的,默认x、width均和titleLabel一致,而可以通过这个属性来调整下划线的偏移值。默认为UIEdgeInsetsZero。
-@property(nonatomic, assign) UIEdgeInsets underlineInsets;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.m
deleted file mode 100644
index 02782d3a..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.m
+++ /dev/null
@@ -1,91 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUILinkButton.m
-// QMUIKit
-//
-// Created by QMUI Team on 2018/4/9.
-//
-
-#import "QMUILinkButton.h"
-#import "QMUICore.h"
-#import "CALayer+QMUI.h"
-
-@interface QMUILinkButton ()
-
-@property(nonatomic, strong) CALayer *underlineLayer;
-@end
-
-@implementation QMUILinkButton
-
-- (instancetype)initWithFrame:(CGRect)frame {
- if (self = [super initWithFrame:frame]) {
- [self didInitialize];
- }
- return self;
-}
-
-- (instancetype)initWithCoder:(NSCoder *)aDecoder {
- if (self = [super initWithCoder:aDecoder]) {
- [self didInitialize];
- }
- return self;
-}
-
-- (void)didInitialize {
- [super didInitialize];
-
- self.underlineLayer = [CALayer layer];
- [self.underlineLayer qmui_removeDefaultAnimations];
- [self.layer addSublayer:self.underlineLayer];
-
- self.underlineHidden = NO;
- self.underlineWidth = 1;
- self.underlineColor = nil;
- self.underlineInsets = UIEdgeInsetsZero;
-}
-
-- (void)setUnderlineHidden:(BOOL)underlineHidden {
- _underlineHidden = underlineHidden;
- self.underlineLayer.hidden = underlineHidden;
-}
-
-- (void)setUnderlineWidth:(CGFloat)underlineWidth {
- _underlineWidth = underlineWidth;
- [self setNeedsLayout];
-}
-
-- (void)setUnderlineColor:(UIColor *)underlineColor {
- _underlineColor = underlineColor;
- [self updateUnderlineColor];
-}
-
-- (void)setUnderlineInsets:(UIEdgeInsets)underlineInsets {
- _underlineInsets = underlineInsets;
- [self setNeedsLayout];
-}
-
-- (void)layoutSubviews {
- [super layoutSubviews];
- if (!self.underlineLayer.hidden) {
- self.underlineLayer.frame = CGRectMake(self.underlineInsets.left, CGRectGetMaxY(self.titleLabel.frame) + self.underlineInsets.top - self.underlineInsets.bottom, CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.underlineInsets), self.underlineWidth);
- }
-}
-
-- (void)setTitleColor:(UIColor *)color forState:(UIControlState)state {
- [super setTitleColor:color forState:state];
- [self updateUnderlineColor];
-}
-
-- (void)updateUnderlineColor {
- UIColor *color = self.underlineColor ? : [self titleColorForState:UIControlStateNormal];
- self.underlineLayer.backgroundColor = color.CGColor;
-}
-
-@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h
index d83cd54e..e46a483f 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUINavigationButton.h
@@ -37,6 +37,12 @@ typedef NS_ENUM(NSUInteger, QMUINavigationButtonType) {
*/
@property(nonatomic, assign, readonly) QMUINavigationButtonType type;
+/**
+ * UIBarButtonItem 默认都是跟随 tintColor 的,所以这里声明是否让图片也是用 AlwaysTemplate 模式
+ * 默认为 YES
+ */
+@property(nonatomic, assign) BOOL adjustsImageTintColorAutomatically;
+
/**
* 导航栏按钮的初始化函数,指定的初始化方法
* @param type 按钮类型
@@ -60,13 +66,18 @@ typedef NS_ENUM(NSUInteger, QMUINavigationButtonType) {
@interface UIBarButtonItem (QMUINavigationButton)
-+ (instancetype)qmui_itemWithButton:(nullable QMUINavigationButton *)button target:(nullable id)target action:(nullable SEL)action;
-+ (instancetype)qmui_itemWithImage:(nullable UIImage *)image target:(nullable id)target action:(nullable SEL)action;
-+ (instancetype)qmui_itemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action;
-+ (instancetype)qmui_itemWithBoldTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action;
++ (instancetype)qmui_itemWithButton:(QMUINavigationButton *)button target:(nullable id)target action:(nullable SEL)action;
++ (instancetype)qmui_itemWithImage:(UIImage *)image target:(nullable id)target action:(nullable SEL)action;
++ (instancetype)qmui_itemWithTitle:(NSString *)title target:(nullable id)target action:(nullable SEL)action;
++ (instancetype)qmui_itemWithBoldTitle:(NSString *)title target:(nullable id)target action:(nullable SEL)action;
+ (instancetype)qmui_backItemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action;
+
+/// 返回一个返回按钮,该返回按钮的文字由配置表 NeedsBackBarButtonItemTitle 和 target 的值决定,如果 NeedsBackBarButtonItemTitle 为 NO,则返回按钮不显示文字,若为 YES,则默认文字为“返回”,但如果 target 为 UIViewController 则会自动获取上一个界面的 title 作为当前返回按钮的文字。
+ (instancetype)qmui_backItemWithTarget:(nullable id)target action:(nullable SEL)action;
+
+/// 返回一个以“×”为图片的关闭按钮,“x”的图片使用配置表 NavBarCloseButtonImage 设置
+ (instancetype)qmui_closeItemWithTarget:(nullable id)target action:(nullable SEL)action;
+
+ (instancetype)qmui_fixedSpaceItemWithWidth:(CGFloat)width;
+ (instancetype)qmui_flexibleSpaceItem;
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m
index 2b5a24a5..b79bdebb 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUINavigationButton.m
@@ -16,10 +16,16 @@
#import "QMUINavigationButton.h"
#import "QMUICore.h"
#import "UIImage+QMUI.h"
+#import "UIColor+QMUI.h"
#import "UIViewController+QMUI.h"
#import "QMUINavigationController.h"
-#import "QMUILog.h"
#import "UIControl+QMUI.h"
+#import "UIView+QMUI.h"
+#import "NSString+QMUI.h"
+#import "UINavigationController+QMUI.h"
+#import "UINavigationItem+QMUI.h"
+#import "UINavigationBar+QMUI.h"
+#import "NSArray+QMUI.h"
typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) {
QMUINavigationButtonPositionNone = -1, // 不处于navigationBar最左(右)边的按钮,则使用None。用None则不会在alignmentRectInsets里调整位置
@@ -75,11 +81,13 @@ - (void)renderButtonStyle {
self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
self.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
self.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES;
+
+ // UIBarButtonItem 默认都是跟随 tintColor 的,所以这里让图片也是用 alwaysTemplate 模式
+ self.adjustsImageTintColorAutomatically = YES;
+
if (self.type == QMUINavigationButtonTypeImage) {
- if (@available(iOS 11, *)) {
- // 让 iOS 11 及以后也能走到 alignmentRectInsets,iOS 10 及以前的系统就算不置为 NO 也可以走到 alignmentRectInsets,从而保证 image 类型的按钮的布局、间距与系统的保持一致
- self.translatesAutoresizingMaskIntoConstraints = NO;
- }
+ // 让 iOS 11 及以后也能走到 alignmentRectInsets,iOS 10 及以前的系统就算不置为 NO 也可以走到 alignmentRectInsets,从而保证 image 类型的按钮的布局、间距与系统的保持一致
+ self.translatesAutoresizingMaskIntoConstraints = NO;
}
// 系统默认对 highlighted 和 disabled 的图片的表现是变身色,但 UIBarButtonItem 是 alpha,为了与 UIBarButtonItem 表现一致,这里禁用了 UIButton 默认的行为,然后通过重写 setImage:forState:,自动将 normal image 处理为对应的 highlighted image 和 disabled image
@@ -101,13 +109,11 @@ - (void)renderButtonStyle {
}
break;
case QMUINavigationButtonTypeBack: {
- UIImage *backIndicatorImage = [UINavigationBar appearance].backIndicatorImage;
+ self.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -24, -24);
+ UIImage *backIndicatorImage = UINavigationBar.qmui_appearanceConfigured.backIndicatorImage;
if (!backIndicatorImage) {
// 配置表没有自定义的图片,则按照系统的返回按钮图片样式创建一张,颜色按照 tintColor 来
- UIColor *tintColor = QMUICMIActivated ? NavBarTintColor : ({
- UIView *view = [[UIView alloc] init];
- view.tintColor;
- });
+ UIColor *tintColor = QMUICMIActivated ? NavBarTintColor : UIColor.qmui_systemTintColor;
backIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(13, 23) lineWidth:3 tintColor:tintColor];
}
[self setImage:backIndicatorImage forState:UIControlStateNormal];
@@ -117,10 +123,10 @@ - (void)renderButtonStyle {
self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
// @warning 这些数值都是每个iOS版本核对过没问题的,如果修改则要检查要每个版本里与系统UIBarButtonItem的布局是否一致
- UIOffset titleOffsetBaseOnSystem = UIOffsetMake(IOS_VERSION >= 11.0 ? 6 : 7, 0);// 经过这些数值的调整后,自定义返回按钮的位置才能和系统默认返回按钮的位置对准,而配置表里设置的值是在这个调整的基础上再调整
+ UIOffset titleOffsetBaseOnSystem = UIOffsetMake(6, 0);// 经过这些数值的调整后,自定义返回按钮的位置才能和系统默认返回按钮的位置对准,而配置表里设置的值是在这个调整的基础上再调整
UIOffset configurationOffset = NavBarBarBackButtonTitlePositionAdjustment;
self.titleEdgeInsets = UIEdgeInsetsMake(titleOffsetBaseOnSystem.vertical + configurationOffset.vertical, titleOffsetBaseOnSystem.horizontal + configurationOffset.horizontal, -titleOffsetBaseOnSystem.vertical - configurationOffset.vertical, -titleOffsetBaseOnSystem.horizontal - configurationOffset.horizontal);
- self.contentEdgeInsets = UIEdgeInsetsMake(IOS_VERSION < 11.0 ? 1 : 0,// iOS 11 以前,y 值偏移一点
+ self.contentEdgeInsets = UIEdgeInsetsMake(0,
0,
0,
self.titleEdgeInsets.left);
@@ -133,11 +139,11 @@ - (void)renderButtonStyle {
}
- (void)setImage:(UIImage *)image forState:(UIControlState)state {
+ if (image && self.adjustsImageTintColorAutomatically) {
+ image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ }
+
if (image && [self imageForState:state] != image) {
- if (image.renderingMode == UIImageRenderingModeAutomatic) {
- // UIBarButtonItem 默认都是跟随 tintColor 的,所以这里让图片也是用 AlwaysTemplate 模式
- image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
- }
if (state == UIControlStateNormal) {
// 将 normal image 处理成对应的 highlighted image 和 disabled image
self.defaultHighlightedImage = [[image qmui_imageWithAlpha:NavBarHighlightedAlpha] imageWithRenderingMode:image.renderingMode];
@@ -157,9 +163,40 @@ - (void)setImage:(UIImage *)image forState:(UIControlState)state {
}
}
}
+
[super setImage:image forState:state];
}
+- (void)setAdjustsImageTintColorAutomatically:(BOOL)adjustsImageTintColorAutomatically {
+ BOOL valueDifference = _adjustsImageTintColorAutomatically != adjustsImageTintColorAutomatically;
+ _adjustsImageTintColorAutomatically = adjustsImageTintColorAutomatically;
+
+ if (valueDifference) {
+ [self updateImageRenderingModeIfNeeded];
+ }
+}
+
+- (void)updateImageRenderingModeIfNeeded {
+ if (self.currentImage) {
+ NSArray *states = @[@(UIControlStateNormal), @(UIControlStateHighlighted), @(UIControlStateSelected), @(UIControlStateSelected|UIControlStateHighlighted), @(UIControlStateDisabled)];
+
+ for (NSNumber *number in states) {
+ UIImage *image = [self imageForState:number.unsignedIntegerValue];
+ if (!image) {
+ return;
+ }
+
+ if (self.adjustsImageTintColorAutomatically) {
+ // 这里的 setImage: 操作不需要使用 renderingMode 对 image 重新处理,而是放到重写的 setImage:forState 里去做就行了
+ [self setImage:image forState:[number unsignedIntegerValue]];
+ } else {
+ // 如果不需要用 template 的模式渲染,并且之前是使用 template 的,则把 renderingMode 改回 original
+ [self setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:[number unsignedIntegerValue]];
+ }
+ }
+ }
+}
+
// 自定义nav按钮,需要根据这个来修改title的三态颜色。
- (void)tintColorDidChange {
[super tintColorDidChange];
@@ -174,16 +211,6 @@ - (UIEdgeInsets)alignmentRectInsets {
UIEdgeInsets insets = [super alignmentRectInsets];
if (self.type == QMUINavigationButtonTypeNormal || self.type == QMUINavigationButtonTypeBold) {
- // 文字类型的按钮,分别对最左、最右那个按钮调整 inset(这里与 UINavigationItem(QMUINavigationButton) 里的 position 赋值配合使用)
- if (@available(iOS 10, *)) {
- } else {
- if (self.buttonPosition == QMUINavigationButtonPositionLeft) {
- insets.left = 8;
- } else if (self.buttonPosition == QMUINavigationButtonPositionRight) {
- insets.right = 8;
- }
- }
-
// 对于奇数大小的字号,不同 iOS 版本的偏移策略不同,统一一下
if (self.titleLabel.font.pointSize / 2.0 > 0) {
insets.top = -PixelOne;
@@ -200,10 +227,6 @@ - (UIEdgeInsets)alignmentRectInsets {
insets.top = 1;
} else if (self.type == QMUINavigationButtonTypeBack) {
insets.top = PixelOne;
- if (@available(iOS 11, *)) {
- } else {
- insets.left = 8;
- }
}
return insets;
@@ -213,27 +236,31 @@ - (UIEdgeInsets)alignmentRectInsets {
@implementation UIBarButtonItem (QMUINavigationButton)
-+ (instancetype)qmui_itemWithButton:(nullable QMUINavigationButton *)button target:(nullable id)target action:(nullable SEL)action {
++ (instancetype)qmui_itemWithButton:(QMUINavigationButton *)button target:(nullable id)target action:(nullable SEL)action {
+ if (!button) return nil;
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
- return [[UIBarButtonItem alloc] initWithCustomView:button];
+ return [[self alloc] initWithCustomView:button];
}
-+ (instancetype)qmui_itemWithImage:(nullable UIImage *)image target:(nullable id)target action:(nullable SEL)action {
- return [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:target action:action];
++ (instancetype)qmui_itemWithImage:(UIImage *)image target:(nullable id)target action:(nullable SEL)action {
+ if (!image) return nil;
+ return [[self alloc] initWithImage:image style:UIBarButtonItemStylePlain target:target action:action];
}
-+ (instancetype)qmui_itemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action {
- return [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action];
++ (instancetype)qmui_itemWithTitle:(NSString *)title target:(nullable id)target action:(nullable SEL)action {
+ if (!title) return nil;
+ return [[self alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action];
}
-+ (instancetype)qmui_itemWithBoldTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action {
- return [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleDone target:target action:action];
++ (instancetype)qmui_itemWithBoldTitle:(NSString *)title target:(nullable id)target action:(nullable SEL)action {
+ if (!title) return nil;
+ return [[self alloc] initWithTitle:title style:UIBarButtonItemStyleDone target:target action:action];
}
+ (instancetype)qmui_backItemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action {
QMUINavigationButton *button = [[QMUINavigationButton alloc] initWithType:QMUINavigationButtonTypeBack title:title];
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
- UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
+ UIBarButtonItem *barButtonItem = [[self alloc] initWithCustomView:button];
return barButtonItem;
}
@@ -247,9 +274,9 @@ + (instancetype)qmui_backItemWithTarget:(nullable id)target action:(nullable SEL
if (previousViewController.navigationItem.backBarButtonItem) {
// 如果前一个界面有主动设置返回按钮的文字,则取这个文字
backTitle = previousViewController.navigationItem.backBarButtonItem.title;
- } else if ([viewController respondsToSelector:@selector(backBarButtonItemTitleWithPreviousViewController:)]) {
+ } else if ([viewController respondsToSelector:@selector(qmui_backBarButtonItemTitleWithPreviousViewController:)]) {
// 否则看是否有通过 QMUI 提供的接口来设置返回按钮的文字,有就用它的值
- backTitle = [((UIViewController *)viewController) backBarButtonItemTitleWithPreviousViewController:previousViewController];
+ backTitle = [((UIViewController *)viewController) qmui_backBarButtonItemTitleWithPreviousViewController:previousViewController];
} else if (previousViewController.title) {
// 否则取上一个界面的标题
backTitle = previousViewController.title;
@@ -259,23 +286,23 @@ + (instancetype)qmui_backItemWithTarget:(nullable id)target action:(nullable SEL
backTitle = @" ";
}
- return [UIBarButtonItem qmui_backItemWithTitle:backTitle target:target action:action];
+ return [self qmui_backItemWithTitle:backTitle target:target action:action];
}
+ (instancetype)qmui_closeItemWithTarget:(nullable id)target action:(nullable SEL)action {
- UIBarButtonItem *closeItem = [[UIBarButtonItem alloc] initWithImage:NavBarCloseButtonImage style:UIBarButtonItemStylePlain target:target action:action];
+ UIBarButtonItem *closeItem = [[self alloc] initWithImage:NavBarCloseButtonImage style:UIBarButtonItemStylePlain target:target action:action];
closeItem.accessibilityLabel = @"关闭";
return closeItem;
}
+ (instancetype)qmui_fixedSpaceItemWithWidth:(CGFloat)width {
- UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:NULL];
+ UIBarButtonItem *item = [[self alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:NULL];
item.width = width;
return item;
}
+ (instancetype)qmui_flexibleSpaceItem {
- return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL];
+ return [[self alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL];
}
@end
@@ -292,24 +319,14 @@ @interface UIBarButtonItem (QMUINavigationButton_Private)
@property(nonatomic, strong, readonly) QMUINavigationButton *qmui_navigationButton;
@end
-@interface UINavigationItem (QMUINavigationButton)
-
-@property(nonatomic, weak, readonly) UINavigationBar *qmui_navigationBar;
-@property(nonatomic, copy) NSArray *tempLeftBarButtonItems;
-@property(nonatomic, copy) NSArray *tempRightBarButtonItems;
-@end
-
@interface UIViewController (QMUINavigationButton)
@end
@interface UINavigationBar (QMUINavigationButton)
-/// 获取 navigationBar 内部的 contentView
-@property(nonatomic, weak, readonly) UIView *qmui_contentView;
-
/// 判断当前的 UINavigationBar 的返回按钮是不是自定义的
-@property(nonatomic, assign) BOOL qmui_customizingBackBarButtonItem;
+@property(nonatomic, readonly) BOOL qmui_customizingBackBarButtonItem;
@end
@implementation UIBarButtonItem (QMUINavigationButton_Private)
@@ -336,9 +353,6 @@ - (QMUINavigationButton *)qmui_navigationButton {
@implementation UINavigationItem (QMUINavigationButton)
-QMUISynthesizeIdCopyProperty(tempLeftBarButtonItems, setTempLeftBarButtonItems)
-QMUISynthesizeIdCopyProperty(tempRightBarButtonItems, setTempRightBarButtonItems)
-
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@@ -347,69 +361,23 @@ + (void)load {
@selector(setLeftBarButtonItems:animated:),
@selector(setRightBarButtonItem:animated:),
@selector(setRightBarButtonItems:animated:),
-
- // 如果被拦截,则 getter 也要返回被缓存的 item,否则会出现这个 bug:https://github.com/Tencent/QMUI_iOS/issues/362
- @selector(leftBarButtonItem),
- @selector(leftBarButtonItems),
- @selector(rightBarButtonItem),
- @selector(rightBarButtonItems)
};
for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) {
SEL originalSelector = selectors[index];
SEL swizzledSelector = NSSelectorFromString([@"qmui_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
- ExchangeImplementations([self class], originalSelector, swizzledSelector);
+ ExchangeImplementations([UINavigationItem class], originalSelector, swizzledSelector);
}
});
}
-// 监控是否在 iOS 10 及以下,手势返回的过程中,手势返回背后的那个界面修改了 navigationItem,这可能导致 bug:https://github.com/Tencent/QMUI_iOS/issues/302
-- (BOOL)detectSetItemsWhenPopping {
- if (@available(iOS 11, *)) {
- } else {
- if (self.qmui_navigationBar && [self.qmui_navigationBar.delegate isKindOfClass:[UINavigationController class]]) {
- UINavigationController *navController = (UINavigationController *)self.qmui_navigationBar.delegate;
-
-// QMUILog(@"UINavigationItem (QMUINavigationButton)", @"navigationController is %@, topViewController is %@, viewControllers is %@, willAppearByInteractivePopGestureRecognizer is %@, navigationControllerPopGestureRecognizerChanging is %@", navController, navController.topViewController, navController.viewControllers, StringFromBOOL(navController.topViewController.qmui_willAppearByInteractivePopGestureRecognizer), StringFromBOOL(navController.topViewController.qmui_navigationControllerPopGestureRecognizerChanging));
-
- if (navController.topViewController.qmui_willAppearByInteractivePopGestureRecognizer && navController.topViewController.qmui_navigationControllerPopGestureRecognizerChanging) {
- // 注意,判断条件里的 qmui_navigationControllerPopGestureRecognizerChanging 关键在于,它是在 viewWillAppear: 执行后才被置为 YES,而 QMUICommonViewController 是在 viewWillAppear: 里调用 setNavigationItems:,所以刚好过滤了这种场景。因为测试过,在 viewWillAppear: 里操作 items 是没问题的,但在那之后的操作就会有问题。
- QMUILog(@"UINavigationItem (QMUINavigationButton)", @"拦截了一次可能产生顶部按钮混乱的操作");
- return YES;
- }
- }
- }
- return NO;
-}
-
- (void)qmui_setLeftBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated {
- if ([self detectSetItemsWhenPopping]) {
- self.tempLeftBarButtonItems = item ? @[item] : nil;
- return;
- }
-
[self qmui_setLeftBarButtonItem:item animated:animated];
// 自动给 position 赋值
item.qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionLeft;
-
- // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279
- if (@available(iOS 11, *)) {
- UINavigationBar *navigationBar = self.qmui_navigationBar;
- if (!navigationBar) return;
-
- // 这个保护对应这个 issue:https://github.com/Tencent/QMUI_iOS/issues/335
- if ([navigationBar.items containsObject:self] && navigationBar.topItem != self) return;
-
- navigationBar.qmui_customizingBackBarButtonItem = item.qmui_isCustomizedBackBarButtonItem;
- }
}
- (void)qmui_setLeftBarButtonItems:(NSArray *)items animated:(BOOL)animated {
- if ([self detectSetItemsWhenPopping]) {
- self.tempLeftBarButtonItems = items;
- return;
- }
-
[self qmui_setLeftBarButtonItems:items animated:animated];
// 自动给 position 赋值
@@ -420,33 +388,9 @@ - (void)qmui_setLeftBarButtonItems:(NSArray *)items animated:
items[i].qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionNone;
}
}
-
- // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279
- if (@available(iOS 11, *)) {
-
- UINavigationBar *navigationBar = self.qmui_navigationBar;
- if (!navigationBar) return;
-
- // 这个保护对应这个 issue:https://github.com/Tencent/QMUI_iOS/issues/335
- if ([navigationBar.items containsObject:self] && navigationBar.topItem != self) return;
-
- BOOL customizingBackBarButtonItem = NO;
- for (UIBarButtonItem *item in items) {
- if (item.qmui_isCustomizedBackBarButtonItem) {
- customizingBackBarButtonItem = YES;
- break;
- }
- }
- navigationBar.qmui_customizingBackBarButtonItem = customizingBackBarButtonItem;
- }
}
- (void)qmui_setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated {
- if ([self detectSetItemsWhenPopping]) {
- self.tempRightBarButtonItems = item ? @[item] : nil;
- return;
- }
-
[self qmui_setRightBarButtonItem:item animated:animated];
// 自动给 position 赋值
@@ -454,11 +398,6 @@ - (void)qmui_setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animat
}
- (void)qmui_setRightBarButtonItems:(NSArray *)items animated:(BOOL)animated {
- if ([self detectSetItemsWhenPopping]) {
- self.tempRightBarButtonItems = items;
- return;
- }
-
[self qmui_setRightBarButtonItems:items animated:animated];
// 自动给 position 赋值
@@ -471,42 +410,6 @@ - (void)qmui_setRightBarButtonItems:(NSArray *)items animated
}
}
-- (UIBarButtonItem *)qmui_leftBarButtonItem {
- if (self.tempLeftBarButtonItems) {
- return self.tempLeftBarButtonItems.firstObject;
- }
- return [self qmui_leftBarButtonItem];
-}
-
-- (NSArray *)qmui_leftBarButtonItems {
- if (self.tempLeftBarButtonItems) {
- return self.tempLeftBarButtonItems;
- }
- return [self qmui_leftBarButtonItems];
-}
-
-- (UIBarButtonItem *)qmui_rightBarButtonItem {
- if (self.tempRightBarButtonItems) {
- return self.tempRightBarButtonItems.firstObject;
- }
- return [self qmui_rightBarButtonItem];
-}
-
-- (NSArray *)qmui_rightBarButtonItems {
- if (self.tempRightBarButtonItems) {
- return self.tempRightBarButtonItems;
- }
- return [self qmui_rightBarButtonItems];
-}
-
-- (UINavigationBar *)qmui_navigationBar {
- // UINavigationItem 内部有个方法可以获取 navigationBar
- if ([self respondsToSelector:@selector(navigationBar)]) {
- return [self performSelector:@selector(navigationBar)];
- }
- return nil;
-}
-
@end
@implementation UIViewController (QMUINavigationButton)
@@ -514,15 +417,26 @@ @implementation UIViewController (QMUINavigationButton)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewDidAppear:), BOOL, ^(UIViewController *selfObject, BOOL firstArgv) {
- if (selfObject.navigationItem.tempLeftBarButtonItems) {
- selfObject.navigationItem.leftBarButtonItems = selfObject.navigationItem.tempLeftBarButtonItems;
- selfObject.navigationItem.tempLeftBarButtonItems = nil;
- }
- if (selfObject.navigationItem.tempRightBarButtonItems) {
- selfObject.navigationItem.rightBarButtonItems = selfObject.navigationItem.tempRightBarButtonItems;
- selfObject.navigationItem.tempRightBarButtonItems = nil;
- }
+
+ // 当使用自定义返回按钮时,无法使用 VoiceOver 或者 iOS 13.4 新增的 Full Keyboard Access 返回
+ OverrideImplementation([UIViewController class], @selector(accessibilityPerformEscape), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^BOOL(UIViewController *selfObject) {
+
+ if (selfObject.navigationItem.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem
+ && ((QMUINavigationButton *)selfObject.navigationItem.leftBarButtonItem.customView).enabled
+ && selfObject.navigationController.qmui_rootViewController != selfObject
+ && selfObject.navigationController.interactivePopGestureRecognizer.enabled
+ && !UIApplication.sharedApplication.ignoringInteractionEvents) {
+ [selfObject.navigationController popViewControllerAnimated:YES];
+ return YES;
+ }
+
+ // call super
+ BOOL (*originSelectorIMP)(id, SEL);
+ originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider();
+ BOOL result = originSelectorIMP(selfObject, originCMD);
+ return result;
+ };
});
});
}
@@ -535,116 +449,89 @@ + (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- OverrideImplementation([UINavigationBar class], @selector(pushNavigationItem:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^(UINavigationBar *selfObject, UINavigationItem *item, BOOL animated) {
+ // 强制修改 contentView 的 directionalLayoutMargins.leading,在使用自定义返回按钮时减小 8
+ // Xcode11 beta2 修改私有 view 的 directionalLayoutMargins 会 crash,换个方式
+ // -[_UINavigationBarContentView directionalLayoutMargins]
+ NSString *barContentViewString = [NSString qmui_stringByConcat:@"_", @"UINavigationBar", @"ContentView", nil];
+
+ OverrideImplementation(NSClassFromString(barContentViewString), @selector(directionalLayoutMargins), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^NSDirectionalEdgeInsets(UIView *selfObject) {
- // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279
- BOOL shouldSetTagBeforeCallingSuper = NO;// 如果要 push 进的新 item 本身就是自定义返回按钮,那么要先打好标记再调用 super,否则先调用 super 再打标记,实测只有这样才不会导致跳动
- if (@available(iOS 11, *)) {
- shouldSetTagBeforeCallingSuper = [selfObject isKindOfClass:originClass] && item.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem;
- }
+ // call super
+ NSDirectionalEdgeInsets (*originSelectorIMP)(id, SEL);
+ originSelectorIMP = (NSDirectionalEdgeInsets (*)(id, SEL))originalIMPProvider();
+ NSDirectionalEdgeInsets originResult = originSelectorIMP(selfObject, originCMD);
- if (shouldSetTagBeforeCallingSuper) {
- selfObject.qmui_customizingBackBarButtonItem = item.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem;
+ // get navbar
+ UINavigationBar *navBar = nil;
+ if ([NSStringFromClass([selfObject class]) isEqualToString:barContentViewString] &&
+ [selfObject.superview isKindOfClass:[UINavigationBar class]]) {
+ navBar = (UINavigationBar *)selfObject.superview;
}
- // call super
- void (*originSelectorIMP)(id, SEL, UINavigationItem *, BOOL);
- originSelectorIMP = (void (*)(id, SEL, UINavigationItem *, BOOL))originalIMPProvider();
- originSelectorIMP(selfObject, originCMD, item, animated);
-
- if (!shouldSetTagBeforeCallingSuper) {
- selfObject.qmui_customizingBackBarButtonItem = item.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem;
+ // change insets
+ if (navBar) {
+ NSDirectionalEdgeInsets value = originResult;
+ value.leading -= (navBar.qmui_customizingBackBarButtonItem ? 8 : 0);
+ return value;
}
+
+ return originResult;
};
});
- // 对应 UINavigationController setViewControllers:animated:
- OverrideImplementation([UINavigationBar class], @selector(setItems:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^(UINavigationBar *selfObject, NSArray *items, BOOL animated) {
+ // 系统的 UIBarButtonItem 响应区域比较大,如果用 customView 则响应区域只有 customView.frame 的大小,这里专门扩大它
+ // 对没用 customView 的不处理
+ OverrideImplementation([UINavigationBar class], @selector(hitTest:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^UIView *(UINavigationBar *selfObject, CGPoint firstArgv, UIEvent *secondArgv) {
+
+ // call super
+ UIView * (*originSelectorIMP)(id, SEL, CGPoint, UIEvent *);
+ originSelectorIMP = (UIView * (*)(id, SEL, CGPoint, UIEvent *))originalIMPProvider();
+ UIView * result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv);
+
+ // result 有值意味着该事件本应属于 bar 的,这时候才干预。
+ // 属于 bar 但又分配给容器而不是精准的某个内容 view,此时才考虑扩大点击范围的识别。
+ BOOL hitNothing = result == selfObject.qmui_contentView || [NSStringFromClass(result.class) containsString:@"StackView"];
+ if (!hitNothing) return result;
- if ([selfObject isKindOfClass:originClass]) {
- // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279
- if (@available(iOS 11, *)) {
- selfObject.qmui_customizingBackBarButtonItem = items.lastObject.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem;
+ NSMutableArray *customViews = [[NSMutableArray alloc] init];
+ if (selfObject.topItem.titleView) {
+ [customViews addObject:selfObject.topItem.titleView];
+ }
+ [customViews addObjectsFromArray:[selfObject.topItem.leftBarButtonItems qmui_compactMapWithBlock:^id _Nullable(UIBarButtonItem * _Nonnull item) {
+ return item.customView ?: nil;
+ }]];
+ [customViews addObjectsFromArray:[selfObject.topItem.rightBarButtonItems qmui_compactMapWithBlock:^id _Nullable(UIBarButtonItem * _Nonnull item) {
+ return item.customView ?: nil;
+ }]];
+ UIView *hitTestingView = [customViews qmui_firstMatchWithBlock:^BOOL(UIView * _Nonnull item) {
+ if (!CGRectIsEmpty(item.frame) && !item.hidden && item.alpha > 0.01 && item.window) {
+ if ([item isKindOfClass:UIControl.class] && !((UIControl *)item).enabled) {
+ return NO;
+ }
+ CGRect rect = [selfObject convertRect:item.bounds fromView:item];
+ rect = CGRectInsetEdges(rect, item.qmui_outsideEdge);
+ if (CGRectContainsPoint(rect, firstArgv)) {
+ return YES;
+ }
}
+ return NO;
+ }];
+ if (hitTestingView) {
+ return hitTestingView;
}
-
- // call super
- void (*originSelectorIMP)(id, SEL, NSArray *, BOOL);
- originSelectorIMP = (void (*)(id, SEL, NSArray *, BOOL))originalIMPProvider();
- originSelectorIMP(selfObject, originCMD, items, animated);
+ return result;
};
});
-
- // 强制修改 contentView 的 directionalLayoutMargins.leading,在使用自定义返回按钮时减小 8
- if (@available(iOS 11, *)) {
- ExtendImplementationOfVoidMethodWithoutArguments([UINavigationBar class], @selector(layoutSubviews), ^(UINavigationBar *selfObject) {
- UIView *contentView = selfObject.qmui_contentView;
- if (contentView) {
- NSDirectionalEdgeInsets value = contentView.directionalLayoutMargins;
- value.leading = value.trailing - (selfObject.qmui_customizingBackBarButtonItem ? 8 : 0);
- contentView.directionalLayoutMargins = value;
- }
- });
- }
});
}
-- (UIView *)qmui_contentView {
- for (UIView *subview in self.subviews) {
- if ([NSStringFromClass(subview.class) containsString:@"ContentView"]) {
- return subview;
- }
- }
- return nil;
-}
-
-static char kAssociatedObjectKey_customizingBackBarButtonItem;
-- (void)setQmui_customizingBackBarButtonItem:(BOOL)qmui_customizingBackBarButtonItem {
- objc_setAssociatedObject(self, &kAssociatedObjectKey_customizingBackBarButtonItem, @(qmui_customizingBackBarButtonItem), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-
- // 触发 UINavigationBar 的 layoutSubviews,在那里面会去更新 contentView 的 layoutMargins
- if (@available(iOS 11, *)) {
- [self setNeedsLayout];
- [self layoutIfNeeded];
- }
-}
-
- (BOOL)qmui_customizingBackBarButtonItem {
- return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_customizingBackBarButtonItem)) boolValue];
-}
-
-@end
-
-@interface UINavigationController (QMUINavigationButton)
-
-@end
-
-@implementation UINavigationController (QMUINavigationButton)
-
-+ (void)load {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
-
- // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279
- if (@available(iOS 11, *)) {
-
- ExtendImplementationOfNonVoidMethodWithTwoArguments([UINavigationController class], @selector(navigationBar:shouldPopItem:), UINavigationBar *, UINavigationItem *, BOOL, ^BOOL(UINavigationController *selfObject, UINavigationBar *navigationBar, UINavigationItem *item, BOOL originReturnValue) {
- if (originReturnValue) {
- BOOL isPopedByCoding = item != navigationBar.topItem;
- if (!isPopedByCoding) {
- if (navigationBar.items.count == 2 && !navigationBar.backItem.leftBarButtonItem && item.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem) {
- // 如果从自定义返回按钮界面返回到根界面,且根界面左上角没有按钮,则不调整 layout,不然会有跳动。理论上不应该这么改,但暂时没想到优雅的解决方式
- } else {
- navigationBar.qmui_customizingBackBarButtonItem = navigationBar.backItem.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem;
- }
- }
- }
- return originReturnValue;
- });
- }
- });
+ if (self.topItem.leftBarButtonItem) {
+ return self.topItem.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem;
+ }
+ return NO;
}
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h
index 313f6211..50175867 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIToolbarButton.h
@@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, QMUIToolbarButtonType) {
QMUIToolbarButtonTypeNormal, // 普通工具栏按钮
QMUIToolbarButtonTypeRed, // 工具栏红色按钮,用于删除等警告性操作
- QMUIToolbarButtonTypeImage, // 图标类型的按钮
+ QMUIToolbarButtonTypeImage, // 图标类型的按钮
};
/**
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m
index a20ca606..6a7b5652 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIToolbarButton.m
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h
index ecd1b3f5..cd123fd7 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUICellHeightCache.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m
index f1dee38f..75875590 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUICellHeightCache.m
@@ -17,6 +17,7 @@
#import "QMUITableViewProtocols.h"
#import "QMUICore.h"
#import "UIScrollView+QMUI.h"
+#import "UITableView+QMUI.h"
#import "UIView+QMUI.h"
#import "NSNumber+QMUI.h"
@@ -153,7 +154,7 @@ - (QMUICellHeightCache *)qmui_keyedHeightCache {
if (!self.qmuiTableCache_allKeyedHeightCaches) {
self.qmuiTableCache_allKeyedHeightCaches = [[NSMutableDictionary alloc] init];
}
- CGFloat contentWidth = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset);
+ CGFloat contentWidth = self.qmui_validContentWidth;
QMUICellHeightCache *cache = self.qmuiTableCache_allKeyedHeightCaches[@(contentWidth)];
if (!cache) {
cache = [[QMUICellHeightCache alloc] init];
@@ -162,9 +163,9 @@ - (QMUICellHeightCache *)qmui_keyedHeightCache {
return cache;
}
-- (void)qmui_invalidateHeightForKey:(id)key {
+- (void)qmui_invalidateHeightForKey:(id)aKey {
[self.qmuiTableCache_allKeyedHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightCache * _Nonnull obj, BOOL * _Nonnull stop) {
- [obj invalidateHeightForKey:key];
+ [obj invalidateHeightForKey:aKey];
}];
}
@@ -179,7 +180,7 @@ - (QMUICellHeightIndexPathCache *)qmui_indexPathHeightCache {
if (!self.qmuiTableCache_allIndexPathHeightCaches) {
self.qmuiTableCache_allIndexPathHeightCaches = [[NSMutableDictionary alloc] init];
}
- CGFloat contentWidth = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset);
+ CGFloat contentWidth = self.qmui_validContentWidth;
QMUICellHeightIndexPathCache *cache = self.qmuiTableCache_allIndexPathHeightCaches[@(contentWidth)];
if (!cache) {
cache = [[QMUICellHeightIndexPathCache alloc] init];
@@ -221,7 +222,7 @@ + (void)load {
for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
SEL originalSelector = selectors[index];
SEL swizzledSelector = NSSelectorFromString([@"qmuiTableCache_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
- ExchangeImplementations([self class], originalSelector, swizzledSelector);
+ ExchangeImplementations([UITableView class], originalSelector, swizzledSelector);
}
});
}
@@ -366,7 +367,7 @@ - (void)qmuiTableCache_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndex
@implementation UITableView (QMUILayoutCell)
- (__kindof UITableViewCell *)templateCellForReuseIdentifier:(NSString *)identifier {
- NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
+ QMUIAssert(identifier.length > 0, @"QMUICellHeightCache", @"%s 需要一个合法的 identifier", __func__);
NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
if (!templateCellsByIdentifiers) {
templateCellsByIdentifiers = @{}.mutableCopy;
@@ -382,8 +383,8 @@ - (__kindof UITableViewCell *)templateCellForReuseIdentifier:(NSString *)identif
// 没有的话,则需要通过register来注册一个cell,否则会crash
if (!templateCell) {
templateCell = [self dequeueReusableCellWithIdentifier:identifier];
- NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
}
+ QMUIAssert(templateCell != nil, @"QMUICellHeightCache", @"通过 %s %@ 无法得到一个 cell 对象", __func__, identifier);
templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
templateCellsByIdentifiers[identifier] = templateCell;
}
@@ -391,13 +392,13 @@ - (__kindof UITableViewCell *)templateCellForReuseIdentifier:(NSString *)identif
}
- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(__kindof UITableViewCell *))configuration {
- if (!identifier || CGRectIsEmpty(self.bounds)) {
+ CGFloat contentWidth = self.qmui_validContentWidth;
+ if (!identifier || contentWidth <= 0) {
return 0;
}
UITableViewCell *cell = [self templateCellForReuseIdentifier:identifier];
[cell prepareForReuse];
if (configuration) configuration(cell);
- CGFloat contentWidth = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset);
CGSize fitSize = CGSizeZero;
if (cell && contentWidth > 0) {
fitSize = [cell sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)];
@@ -407,7 +408,7 @@ - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier configuration
// 通过indexPath缓存高度
- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *))configuration {
- if (!identifier || !indexPath || CGRectIsEmpty(self.bounds)) {
+ if (!identifier || !indexPath || self.qmui_validContentWidth <= 0) {
return 0;
}
if ([self.qmui_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
@@ -420,7 +421,7 @@ - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexP
// 通过key缓存高度
- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(__kindof UITableViewCell *))configuration {
- if (!identifier || !key || CGRectIsEmpty(self.bounds)) {
+ if (!identifier || !key || self.qmui_validContentWidth <= 0) {
return 0;
}
if ([self.qmui_keyedHeightCache existsHeightForKey:key]) {
@@ -457,7 +458,7 @@ - (QMUICellHeightCache *)qmui_keyedHeightCache {
if (!self.qmuiCollectionCache_allKeyedHeightCaches) {
self.qmuiCollectionCache_allKeyedHeightCaches = [[NSMutableDictionary alloc] init];
}
- CGSize collectionViewSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.qmui_contentInset));
+ CGSize collectionViewSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.safeAreaInsets), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.safeAreaInsets));
QMUICellHeightCache *cache = self.qmuiCollectionCache_allKeyedHeightCaches[[NSValue valueWithCGSize:collectionViewSize]];
if (!cache) {
cache = [[QMUICellHeightCache alloc] init];
@@ -466,9 +467,9 @@ - (QMUICellHeightCache *)qmui_keyedHeightCache {
return cache;
}
-- (void)qmui_invalidateHeightForKey:(id)key {
+- (void)qmui_invalidateHeightForKey:(id)aKey {
[self.qmuiCollectionCache_allKeyedHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightCache * _Nonnull obj, BOOL * _Nonnull stop) {
- [obj invalidateHeightForKey:key];
+ [obj invalidateHeightForKey:aKey];
}];
}
@@ -483,7 +484,7 @@ - (QMUICellHeightIndexPathCache *)qmui_indexPathHeightCache {
if (!self.qmuiCollectionCache_allIndexPathHeightCaches) {
self.qmuiCollectionCache_allIndexPathHeightCaches = [[NSMutableDictionary alloc] init];
}
- CGSize collectionViewSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.qmui_contentInset));
+ CGSize collectionViewSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.safeAreaInsets), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.safeAreaInsets));
QMUICellHeightIndexPathCache *cache = self.qmuiCollectionCache_allIndexPathHeightCaches[[NSValue valueWithCGSize:collectionViewSize]];
if (!cache) {
cache = [[QMUICellHeightIndexPathCache alloc] init];
@@ -525,7 +526,7 @@ + (void)load {
for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) {
SEL originalSelector = selectors[index];
SEL swizzledSelector = NSSelectorFromString([@"qmuiCollectionCache_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
- ExchangeImplementations([self class], originalSelector, swizzledSelector);
+ ExchangeImplementations([UICollectionView class], originalSelector, swizzledSelector);
}
});
}
@@ -670,9 +671,8 @@ - (void)qmuiCollectionCache_moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath t
@implementation UICollectionView (QMUILayoutCell)
- (__kindof UICollectionViewCell *)templateCellForReuseIdentifier:(NSString *)identifier cellClass:(Class)cellClass {
- NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
- NSAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"only flow layout accept");
- NSAssert([cellClass isSubclassOfClass:[UICollectionViewCell class]], @"must be uicollection view cell");
+ QMUIAssert(identifier.length > 0, @"QMUICellHeightCache", @"%s 需要一个合法的 identifier", __func__);
+ QMUIAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"QMUICellHeightCache", @"只支持 %@", NSStringFromClass(UICollectionViewFlowLayout.class));
NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
if (!templateCellsByIdentifiers) {
templateCellsByIdentifiers = @{}.mutableCopy;
@@ -683,7 +683,7 @@ - (__kindof UICollectionViewCell *)templateCellForReuseIdentifier:(NSString *)id
// CollecionView 跟 TableView 不太一样,无法通过 dequeueReusableCellWithReuseIdentifier:forIndexPath: 来拿到cell(如果这样做,首先indexPath不知道传什么值,其次是这样做会已知crash,说数组越界),所以只能通过传一个class来通过init方法初始化一个cell,但是也有缓存来复用cell。
// templateCell = [self dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
templateCell = [[cellClass alloc] initWithFrame:CGRectZero];
- NSAssert(templateCell != nil, @"Cell must be registered to collection view for identifier - %@", identifier);
+ QMUIAssert(templateCell != nil, @"QMUICellHeightCache", @"通过 %s %@ 无法得到一个 cell 对象", __func__, identifier);
}
templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
templateCellsByIdentifiers[identifier] = templateCell;
@@ -691,7 +691,7 @@ - (__kindof UICollectionViewCell *)templateCellForReuseIdentifier:(NSString *)id
}
- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration {
- if (!identifier || CGRectIsEmpty(self.bounds)) {
+ if (!identifier || CGRectGetWidth(self.bounds) <= 0) {
return 0;
}
UICollectionViewCell *cell = [self templateCellForReuseIdentifier:identifier cellClass:cellClass];
@@ -706,7 +706,7 @@ - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Cl
// 通过indexPath缓存高度
- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration {
- if (!identifier || !indexPath || CGRectIsEmpty(self.bounds)) {
+ if (!identifier || !indexPath || CGRectGetWidth(self.bounds) <= 0) {
return 0;
}
if ([self.qmui_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
@@ -719,7 +719,7 @@ - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Cl
// 通过key缓存高度
- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByKey:(id)key configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration {
- if (!identifier || !key || CGRectIsEmpty(self.bounds)) {
+ if (!identifier || !key || CGRectGetWidth(self.bounds) <= 0) {
return 0;
}
if ([self.qmui_keyedHeightCache existsHeightForKey:key]) {
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h
index 7fc459b4..9c4be7c9 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUICellHeightKeyCache.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m
index 8379b931..fa2e0838 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUICellHeightKeyCache.m
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h
index 866cc4fd..c7ff7dcd 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// UITableView+QMUICellHeightKeyCache.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m
index 2b27470b..a6198636 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// UITableView+QMUICellHeightKeyCache.m
@@ -18,6 +18,7 @@
#import "QMUICellHeightKeyCache.h"
#import "UIView+QMUI.h"
#import "UIScrollView+QMUI.h"
+#import "UITableView+QMUI.h"
#import "QMUITableViewProtocols.h"
#import "QMUIMultipleDelegates.h"
@@ -34,10 +35,7 @@ + (void)load {
OverrideImplementation([UITableView class], @selector(setDelegate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UITableView *selfObject, id firstArgv) {
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- [selfObject replaceMethodForDelegateIfNeeded:firstArgv];
- }
+ [selfObject replaceMethodForDelegateIfNeeded:firstArgv];
// call super
void (*originSelectorIMP)(id, SEL, id);
@@ -54,8 +52,8 @@ - (void)setQmui_cacheCellHeightByKeyAutomatically:(BOOL)qmui_cacheCellHeightByKe
if (qmui_cacheCellHeightByKeyAutomatically) {
- NSAssert(!self.delegate || [self.delegate respondsToSelector:@selector(qmui_tableView:cacheKeyForRowAtIndexPath:)], @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", self.delegate, NSStringFromSelector(@selector(qmui_tableView:cacheKeyForRowAtIndexPath:)));
- NSAssert(self.estimatedRowHeight != 0 || [self.delegate respondsToSelector:@selector(tableView:estimatedHeightForRowAtIndexPath:)], @"必须为 estimatedRowHeight 赋一个不为0的值,或者实现 tableView:estimatedHeightForRowAtIndexPath: 方法,否则无法开启 self-sizing cells 功能");
+ QMUIAssert(!self.delegate || [self.delegate respondsToSelector:@selector(qmui_tableView:cacheKeyForRowAtIndexPath:)], @"QMUICellHeightKeyCache", @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", self.delegate, NSStringFromSelector(@selector(qmui_tableView:cacheKeyForRowAtIndexPath:)));
+ QMUIAssert(self.estimatedRowHeight != 0 || [self.delegate respondsToSelector:@selector(tableView:estimatedHeightForRowAtIndexPath:)], @"QMUICellHeightKeyCache", @"必须为 estimatedRowHeight 赋一个不为0的值,或者实现 tableView:estimatedHeightForRowAtIndexPath: 方法,否则无法开启 self-sizing cells 功能");
[self replaceMethodForDelegateIfNeeded:(id)self.delegate];
@@ -81,7 +79,7 @@ - (void)setQmui_allKeyCaches:(NSMutableDictionary cachedKey = [((id)tableView.delegate) qmui_tableView:tableView cacheKeyForRowAtIndexPath:indexPath];
@@ -130,22 +122,15 @@ - (CGFloat)qmui_tableView:(UITableView *)tableView estimatedHeightForRowAtIndexP
return UITableViewAutomaticDimension;// 表示 QMUICellHeightKeyCache 无法决定一个合适的高度,交给业务,或者交给系统默认值决定。
}
-static NSMutableSet *qmui_methodsReplacedClasses;
- (void)replaceMethodForDelegateIfNeeded:(id)delegate {
if (self.qmui_cacheCellHeightByKeyAutomatically && delegate) {
- if (!qmui_methodsReplacedClasses) {
- qmui_methodsReplacedClasses = [NSMutableSet set];
- }
void (^addSelectorBlock)(id) = ^void(id aDelegate) {
- if ([qmui_methodsReplacedClasses containsObject:NSStringFromClass(aDelegate.class)]) {
- return;
- }
- [qmui_methodsReplacedClasses addObject:NSStringFromClass(aDelegate.class)];
-
- [self handleWillDisplayCellMethodForDelegate:aDelegate];
- [self handleHeightForRowMethodForDelegate:aDelegate];
- [self handleEstimatedHeightForRowMethodForDelegate:aDelegate];
+ [QMUIHelper executeBlock:^{
+ [self handleWillDisplayCellMethodForDelegate:aDelegate];
+ [self handleHeightForRowMethodForDelegate:aDelegate];
+ [self handleEstimatedHeightForRowMethodForDelegate:aDelegate];
+ } oncePerIdentifier:[NSString stringWithFormat:@"QMUICellHeightKeyCache %@", NSStringFromClass(aDelegate.class)]];
};
if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) {
@@ -180,9 +165,6 @@ - (void)handleWillDisplayCellMethodForDelegate:(id)delega
originSelectorIMP = (void (*)(id, SEL, UITableView *, UITableViewCell *, NSIndexPath *))originalIMPProvider();
originSelectorIMP(delegateSelf, originCMD, tableView, cell, indexPath);
- // avoid superclass
- if (![delegateSelf isKindOfClass:originClass]) return;
-
// call QMUI
willDisplayCellFunction(delegateSelf, willDisplayCellSelector, tableView, cell, indexPath);
};
@@ -209,9 +191,6 @@ - (void)handleHeightForRowMethodForDelegate:(id)delegate
originSelectorIMP = (CGFloat (*)(id, SEL, UITableView *, NSIndexPath *))originalIMPProvider();
CGFloat result = originSelectorIMP(delegateSelf, originCMD, tableView, indexPath);
- // avoid superclass
- if (![delegateSelf isKindOfClass:originClass]) return result;
-
if (result >= 0) {
return result;
}
@@ -237,12 +216,9 @@ - (void)handleEstimatedHeightForRowMethodForDelegate:(id)
OverrideImplementation([delegate class], heightForRowSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^CGFloat(id delegateSelf, UITableView *tableView, NSIndexPath *indexPath) {
- CGFloat result = 0;
- if ([delegateSelf isKindOfClass:originClass]) {
- result = heightForRowFunction(delegateSelf, heightForRowSelector, tableView, indexPath);
- if (result != UITableViewAutomaticDimension) {
- return result;
- }
+ CGFloat result = heightForRowFunction(delegateSelf, heightForRowSelector, tableView, indexPath);
+ if (result != UITableViewAutomaticDimension) {
+ return result;
}
// call super
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h
index a865fa60..c97a7c87 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUICellSizeKeyCache.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m
index 79adafea..8f083b4a 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUICellSizeKeyCache.m
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h
index a280777f..8ba4b741 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// UICollectionView+QMUICellSizeKeyCache.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m
index 3fc66847..54b71dae 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// UICollectionView+QMUICellSizeKeyCache.m
@@ -80,10 +80,7 @@ + (void)load {
OverrideImplementation([UICollectionView class], @selector(setDelegate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UICollectionView *selfObject, id firstArgv) {
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- [selfObject replaceMethodForDelegateIfNeeded:firstArgv];
- }
+ [selfObject replaceMethodForDelegateIfNeeded:firstArgv];
// call super
void (*originSelectorIMP)(id, SEL, id);
@@ -99,7 +96,7 @@ - (void)setQmui_cacheCellSizeByKeyAutomatically:(BOOL)qmui_cacheCellSizeByKeyAut
objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiCacheCellSizeByKeyAutomatically, @(qmui_cacheCellSizeByKeyAutomatically), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (qmui_cacheCellSizeByKeyAutomatically) {
- NSAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"QMUICellSizeKeyCache 只支持 UICollectionViewFlowLayout");
+ QMUIAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"QMUICellSizeKeyCache", @"只支持 %@", NSStringFromClass(UICollectionViewFlowLayout.class));
[self replaceMethodForDelegateIfNeeded:self.delegate];
@@ -143,10 +140,10 @@ - (QMUICellSizeKeyCache *)qmui_currentCellSizeKeyCache {
- (CGFloat)widthForCacheKey {
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout;
if (layout.scrollDirection == UICollectionViewScrollDirectionHorizontal) {
- CGFloat height = CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.qmui_contentInset) - UIEdgeInsetsGetVerticalValue(layout.sectionInset);
+ CGFloat height = CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.adjustedContentInset) - UIEdgeInsetsGetVerticalValue(layout.sectionInset);
return height;
}
- CGFloat width = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset) - UIEdgeInsetsGetHorizontalValue(((UICollectionViewFlowLayout *)self.collectionViewLayout).sectionInset);
+ CGFloat width = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.adjustedContentInset) - UIEdgeInsetsGetHorizontalValue(((UICollectionViewFlowLayout *)self.collectionViewLayout).sectionInset);
return width;
}
@@ -154,7 +151,8 @@ - (void)qmui_collectionView:(UICollectionView *)collectionView willDisplayCell:(
[collectionView qmui_collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath];
if (collectionView.qmui_cacheCellSizeByKeyAutomatically) {
if (![collectionView.delegate respondsToSelector:@selector(qmui_collectionView:cacheKeyForItemAtIndexPath:)]) {
- NSAssert(NO, @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", collectionView.delegate, NSStringFromSelector(@selector(qmui_collectionView:cacheKeyForItemAtIndexPath:)));
+ QMUIAssert(NO, @"QMUICellSizeKeyCache", @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", collectionView.delegate, NSStringFromSelector(@selector(qmui_collectionView:cacheKeyForItemAtIndexPath:)));
+ return;
}
id cachedKey = [((id)self) qmui_collectionView:collectionView cacheKeyForItemAtIndexPath:indexPath];
[collectionView.qmui_currentCellSizeKeyCache cacheSize:cell.frame.size forKey:cachedKey];
@@ -180,30 +178,24 @@ - (void)qmui_collectionView:(UICollectionView *)collectionView willDisplayCell:(
// return collectionViewLayout.estimatedItemSize;
//}
-static NSMutableSet *qmui_methodsReplacedClasses;
- (void)replaceMethodForDelegateIfNeeded:(id)delegate {
- if (self.qmui_cacheCellSizeByKeyAutomatically && delegate) {
- if (!qmui_methodsReplacedClasses) {
- qmui_methodsReplacedClasses = [NSMutableSet set];
- }
- void (^addSelectorBlock)(id) = ^void(id aDelegate) {
- if ([qmui_methodsReplacedClasses containsObject:NSStringFromClass(aDelegate.class)]) {
- return;
- }
- [qmui_methodsReplacedClasses addObject:NSStringFromClass(aDelegate.class)];
- };
-
- if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) {
- NSPointerArray *delegates = [((QMUIMultipleDelegates *)delegate).delegates copy];
- for (id d in delegates) {
- if ([d conformsToProtocol:@protocol(UICollectionViewDelegate)]) {
- addSelectorBlock((id)d);
- }
- }
- } else {
- addSelectorBlock((id)delegate);
- }
- }
+// if (self.qmui_cacheCellSizeByKeyAutomatically && delegate) {
+// void (^addSelectorBlock)(id) = ^void(id aDelegate) {
+// [QMUIHelper executeBlock:^{
+// } oncePerIdentifier:[NSString stringWithFormat:@"QMUICellHeightKeyCache collectionView %@", NSStringFromClass(aDelegate.class)]];
+// };
+//
+// if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) {
+// NSPointerArray *delegates = [((QMUIMultipleDelegates *)delegate).delegates copy];
+// for (id d in delegates) {
+// if ([d conformsToProtocol:@protocol(UICollectionViewDelegate)]) {
+// addSelectorBlock((id)d);
+// }
+// }
+// } else {
+// addSelectorBlock((id)delegate);
+// }
+// }
}
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.h b/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.h
new file mode 100644
index 00000000..df36f94b
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.h
@@ -0,0 +1,40 @@
+//
+// QMUICheckbox.h
+// QMUIKit
+//
+// Created by molice on 2024/8/1.
+// Copyright © 2024 QMUI Team. All rights reserved.
+//
+
+#import
+#import "QMUIButton.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// 圆形勾选控件,selected = YES 表示勾选,indeterminate = YES 表示半选,enabled = NO 表示禁用。
+/// 由于父类是 QMUIButton,所以可以通过 setTitle:forState: 轻松实现左边 checkbox 右边说明文本的效果。
+/// 尺寸可以通过 checkboxSize 修改,颜色可通过 tintColor 修改。
+/// 点击勾选的交互需要由业务自己实现。
+@interface QMUICheckbox : QMUIButton
+
+/// 置为半选状态。可以理解为一个 Checkbox 的 indeterminate 和 checked(selected) 是平级的、互斥的,当该属性被设置为 YES 时,会将 selected 置为 NO,当 selected 被置为 YES 时,会将该属性置为 NO。
+@property(nonatomic, assign) BOOL indeterminate;
+
+/// 指定 checkbox 图片的尺寸(如果存在 title,不影响 title 的尺寸)
+/// 默认为(16, 16)
+@property(nonatomic, assign) CGSize checkboxSize;
+
+/// 未勾选的状态,置为 nil 则使用组件默认图
+@property(nonatomic, strong) UIImage *normalImage;
+
+/// 勾选的状态,置为 nil 则使用组件默认图
+@property(nonatomic, strong) UIImage *selectedImage;
+
+/// 半勾选的状态,置为 nil 则使用组件默认图
+@property(nonatomic, strong) UIImage *indeterminateImage;
+
+/// 未勾选且禁用的状态(如果是已勾选的禁用,会直接沿用该状态的图片,只有未勾选的禁用可以有单独的图),置为 nil 则使用组件默认图
+@property(nonatomic, strong) UIImage *disabledImage;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.m b/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.m
new file mode 100644
index 00000000..77d6046e
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.m
@@ -0,0 +1,104 @@
+//
+// QMUICheckbox.m
+// QMUIKit
+//
+// Created by molice on 2024/8/1.
+// Copyright © 2024 QMUI Team. All rights reserved.
+//
+
+#import "QMUICheckbox.h"
+#import "QMUICore.h"
+#import "CALayer+QMUI.h"
+#import "UIView+QMUI.h"
+
+@interface QMUICheckbox ()
+@property(nonatomic, strong) UIImageView *indeterminateImageView;
+@property(nonatomic, strong) CALayer *imageViewMaks;
+@end
+
+@implementation QMUICheckbox
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ self.normalImage = self.normalImage;
+ self.selectedImage = self.selectedImage;
+ self.indeterminateImage = self.indeterminateImage;
+ self.disabledImage = self.disabledImage;
+
+ _checkboxSize = self.currentImage.size;
+ self.imageView.contentMode = UIViewContentModeScaleToFill;
+ self.qmui_outsideEdge = UIEdgeInsetsMake(-8, -8, -8, -8);
+ }
+ return self;
+}
+
+- (void)setNormalImage:(UIImage *)normalImage {
+ _normalImage = normalImage ?: [[QMUIHelper imageWithName:@"QMUI_checkbox16"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ [self setImage:_normalImage forState:UIControlStateNormal];
+}
+
+- (void)setSelectedImage:(UIImage *)selectedImage {
+ _selectedImage = selectedImage ?: [[QMUIHelper imageWithName:@"QMUI_checkbox16_checked"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ [self setImage:_selectedImage forState:UIControlStateSelected];
+ [self setImage:_selectedImage forState:UIControlStateSelected|UIControlStateHighlighted];
+ [self setImage:_selectedImage forState:UIControlStateSelected|UIControlStateDisabled];
+}
+
+- (void)setDisabledImage:(UIImage *)disabledImage {
+ _disabledImage = disabledImage ?: [[QMUIHelper imageWithName:@"QMUI_checkbox16_disabled"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ [self setImage:_disabledImage forState:UIControlStateDisabled];
+}
+
+- (void)setIndeterminateImage:(UIImage *)indeterminateImage {
+ _indeterminateImage = indeterminateImage ?: [[QMUIHelper imageWithName:@"QMUI_checkbox16_indeterminate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+}
+
+- (void)setIndeterminate:(BOOL)indeterminate {
+ BOOL valueChanged = _indeterminate != indeterminate;
+ if (!valueChanged) return;
+
+ _indeterminate = indeterminate;
+ if (indeterminate) {
+ if (self.selected) {
+ self.selected = NO;
+ }
+ if (!self.indeterminateImageView) {
+ self.indeterminateImageView = [[UIImageView alloc] init];
+ self.indeterminateImageView.contentMode = UIViewContentModeScaleToFill;
+ [self addSubview:self.indeterminateImageView];
+ }
+ if (!self.imageViewMaks) {
+ self.imageViewMaks = CALayer.layer;
+ [self.imageViewMaks qmui_removeDefaultAnimations];
+ }
+ self.indeterminateImageView.image = self.indeterminateImage;
+ self.indeterminateImageView.hidden = NO;
+ self.imageView.layer.mask = self.imageViewMaks;// 保持 imageView 布局不变的情况下让 imageView 不可见
+ [self setNeedsLayout];
+ } else {
+ self.indeterminateImageView.hidden = YES;
+ self.imageView.layer.mask = nil;
+ }
+}
+
+- (void)setSelected:(BOOL)selected {
+ [super setSelected:selected];
+ if (selected && self.indeterminate) {
+ self.indeterminate = NO;
+ }
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ self.indeterminateImageView.frame = self.imageView.frame;
+}
+
+- (void)setCheckboxSize:(CGSize)checkboxSize {
+ if (CGSizeIsEmpty(checkboxSize)) return;
+ _checkboxSize = checkboxSize;
+ self.imageView.qmui_fixedSize = checkboxSize;
+ self.indeterminateImageView.qmui_fixedSize = checkboxSize;
+ [self setNeedsLayout];
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h
index c272909c..b532188d 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUICollectionViewPagingLayout.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m
index b0b13587..24f96227 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUICollectionViewPagingLayout.m
@@ -155,9 +155,9 @@ - (void)prepareLayout {
if (self.debugLayer) {
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
- self.debugLayer.frame = CGRectFlatMake(0, self.collectionView.qmui_contentInset.top + self.sectionInset.top + _finalItemSize.height / 2, CGRectGetWidth(self.collectionView.bounds), PixelOne);
+ self.debugLayer.frame = CGRectFlatMake(0, self.collectionView.adjustedContentInset.top + self.sectionInset.top + _finalItemSize.height / 2, CGRectGetWidth(self.collectionView.bounds), PixelOne);
} else {
- self.debugLayer.frame = CGRectFlatMake(self.collectionView.qmui_contentInset.left + self.sectionInset.left + _finalItemSize.width / 2, 0, PixelOne, CGRectGetHeight(self.collectionView.bounds));
+ self.debugLayer.frame = CGRectFlatMake(self.collectionView.adjustedContentInset.left + self.sectionInset.left + _finalItemSize.width / 2, 0, PixelOne, CGRectGetHeight(self.collectionView.bounds));
}
}
}
@@ -232,7 +232,7 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO
CGSize contentSize = self.collectionViewContentSize;
CGSize frameSize = self.collectionView.bounds.size;
- UIEdgeInsets contentInset = self.collectionView.qmui_contentInset;
+ UIEdgeInsets contentInset = self.collectionView.adjustedContentInset;
BOOL scrollingToRight = proposedContentOffset.x < self.collectionView.contentOffset.x;// 代表 collectionView 期望的实际滚动方向是向右,但不代表手指拖拽的方向是向右,因为有可能此时已经在左边的尽头,继续往右拖拽,松手的瞬间由于回弹,这里会判断为是想向左滚动,但其实你的手指是向右拖拽
BOOL scrollingToBottom = proposedContentOffset.y < self.collectionView.contentOffset.y;
@@ -252,11 +252,7 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO
// 最顶/最底
if (proposedContentOffset.y < -contentInset.top || proposedContentOffset.y >= contentSize.height + contentInset.bottom - frameSize.height) {
- if (IOS_VERSION_NUMBER < 100000) {
- // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算
- // https://github.com/Tencent/QMUI_iOS/issues/499
- proposedContentOffset.y = MIN(MAX(proposedContentOffset.y, -contentInset.top), contentSize.height + contentInset.bottom - frameSize.height);
- }
+ // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算
return proposedContentOffset;
}
@@ -269,8 +265,8 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO
} else {
CGFloat remainder = progress - currentIndex;
CGFloat offset = remainder * itemSpacing;
- BOOL shouldNext = (forcePaging && !scrollingToBottom) ? YES : (offset / _finalItemSize.height >= self.pagingThreshold);
- BOOL shouldPrev = (forcePaging && scrollingToBottom) ? YES : (offset / _finalItemSize.height <= 1 - self.pagingThreshold);
+ BOOL shouldNext = (forcePaging || (offset / _finalItemSize.height >= self.pagingThreshold)) && !scrollingToBottom && velocity.y > 0;
+ BOOL shouldPrev = (forcePaging || (offset / _finalItemSize.height <= 1 - self.pagingThreshold)) && scrollingToBottom && velocity.y < 0;
targetIndex = currentIndex + (shouldNext ? 1 : (shouldPrev ? -1 : 0));
}
proposedContentOffset.y = -contentInset.top + targetIndex * itemSpacing;
@@ -287,11 +283,7 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO
// 最左/最右
if (proposedContentOffset.x < -contentInset.left || proposedContentOffset.x >= contentSize.width + contentInset.right - frameSize.width) {
- if (IOS_VERSION_NUMBER < 100000) {
- // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算
- // https://github.com/Tencent/QMUI_iOS/issues/499
- proposedContentOffset.x = MIN(MAX(proposedContentOffset.x, -contentInset.left), contentSize.width + contentInset.right - frameSize.width);
- }
+ // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算
return proposedContentOffset;
}
@@ -304,8 +296,9 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO
} else {
CGFloat remainder = progress - currentIndex;
CGFloat offset = remainder * itemSpacing;
- BOOL shouldNext = (forcePaging && !scrollingToRight) ? YES : (offset / _finalItemSize.width >= self.pagingThreshold);
- BOOL shouldPrev = (forcePaging && scrollingToRight) ? YES : (offset / _finalItemSize.width <= 1 - self.pagingThreshold);
+ // collectionView 关闭了 bounces 后,如果在第一页向左边快速滑动一段距离,并不会触发上一个「最左/最右」的判断(因为 proposedContentOffset 不够),此时的 velocity 为负数,所以要加上 velocity.x > 0 的判断,否则这种情况会命中 forcePaging && !scrollingToRight 这两个条件,当做下一页处理。
+ BOOL shouldNext = (forcePaging || (offset / _finalItemSize.width >= self.pagingThreshold)) && !scrollingToRight && velocity.x > 0;
+ BOOL shouldPrev = (forcePaging || (offset / _finalItemSize.width <= 1 - self.pagingThreshold)) && scrollingToRight && velocity.x < 0;
targetIndex = currentIndex + (shouldNext ? 1 : (shouldPrev ? -1 : 0));
}
proposedContentOffset.x = -contentInset.left + targetIndex * itemSpacing;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h
index 098e0c15..27211b87 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h
@@ -1,17 +1,16 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIConsole.h
// QMUIKit
//
// Created by MoLice on 2019/J/11.
-// Copyright © 2019 QMUI Team. All rights reserved.
//
#import
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m
index aa134404..5e7ed03f 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m
@@ -1,17 +1,16 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIConsole.m
// QMUIKit
//
// Created by MoLice on 2019/J/11.
-// Copyright © 2019 QMUI Team. All rights reserved.
//
#import "QMUIConsole.h"
@@ -22,9 +21,37 @@
#import "UIColor+QMUI.h"
#import "QMUITextView.h"
+/// 定义一个 class 只是为了在 Lookin 里表达这是一个 console window 而已,不需要实现什么东西
+@interface QMUIConsoleWindow : UIWindow
+@end
+
+@implementation QMUIConsoleWindow
+
+- (instancetype)init {
+ if (self = [super init]) {
+ self.backgroundColor = nil;
+ if (QMUICMIActivated) {
+ self.windowLevel = UIWindowLevelQMUIConsole;
+ } else {
+ self.windowLevel = 1;
+ }
+ self.qmui_capturesStatusBarAppearance = NO;
+ }
+ return self;
+}
+
+- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
+ // 当显示 QMUIConsole 时,点击空白区域,consoleViewController hitTest 会 return nil,从而将事件传递给 window,再由 window hitTest return nil 来把事件传递给 UIApplication.delegate.window。但在 iPad 12-inch 里,当 consoleViewController hitTest return nil 后,事件会错误地传递给 consoleViewController.view.superview(而不是 consoleWindow),不清楚原因,暂时做一下保护
+ // https://github.com/Tencent/QMUI_iOS/issues/1169
+ UIView *originalView = [super hitTest:point withEvent:event];
+ return originalView == self || originalView == self.rootViewController.view.superview ? nil : originalView;
+}
+
+@end
+
@interface QMUIConsole ()
-@property(nonatomic, strong) UIWindow *consoleWindow;
+@property(nonatomic, strong) QMUIConsoleWindow *consoleWindow;
@property(nonatomic, strong) QMUIConsoleViewController *consoleViewController;
@end
@@ -66,6 +93,7 @@ + (id)allocWithZone:(struct _NSZone *)zone{
+ (void)logWithLevel:(NSString *)level name:(NSString *)name logString:(id)logString {
QMUIConsole *console = [QMUIConsole sharedInstance];
+ if (!QMUIConsole.sharedInstance.canShow) return;
[console initConsoleWindowIfNeeded];
[console.consoleViewController logWithLevel:level name:name logString:logString];
if (console.showConsoleAutomatically) {
@@ -84,8 +112,19 @@ + (void)clear {
+ (void)show {
QMUIConsole *console = [QMUIConsole sharedInstance];
if (console.canShow) {
- [[QMUIConsole sharedInstance] initConsoleWindowIfNeeded];
- [QMUIConsole sharedInstance].consoleWindow.hidden = NO;
+
+ if (!console.consoleWindow.hidden) return;
+
+ // 在某些情况下 show 的时候刚好界面正在做动画,就可能会看到 consoleWindow 从左上角展开的过程(window 默认背景色是黑色的),所以这里做了一些小处理
+ // https://github.com/Tencent/QMUI_iOS/issues/743
+ [UIView performWithoutAnimation:^{
+ [console initConsoleWindowIfNeeded];
+ console.consoleWindow.alpha = 0;
+ console.consoleWindow.hidden = NO;
+ }];
+ [UIView animateWithDuration:.25 delay:.2 options:QMUIViewAnimationOptionsCurveOut animations:^{
+ console.consoleWindow.alpha = 1;
+ } completion:nil];
}
}
@@ -95,14 +134,7 @@ + (void)hide {
- (void)initConsoleWindowIfNeeded {
if (!self.consoleWindow) {
- self.consoleWindow = [[UIWindow alloc] init];
- self.consoleWindow.backgroundColor = nil;
- self.consoleWindow.qmui_capturesStatusBarAppearance = NO;
- __weak __typeof(self)weakSelf = self;
- self.consoleWindow.qmui_hitTestBlock = ^__kindof UIView * _Nonnull(CGPoint point, UIEvent * _Nonnull event, __kindof UIView * _Nonnull originalView) {
- return originalView == weakSelf.consoleWindow ? nil : originalView;
- };
-
+ self.consoleWindow = [[QMUIConsoleWindow alloc] init];
self.consoleViewController = [[QMUIConsoleViewController alloc] init];
self.consoleWindow.rootViewController = self.consoleViewController;
}
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h
index b82e830e..8419c765 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h
@@ -1,17 +1,16 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIConsoleToolbar.h
// QMUIKit
//
// Created by MoLice on 2019/J/11.
-// Copyright © 2019 QMUI Team. All rights reserved.
//
#import
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m
index 0c172d13..c8958fff 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m
@@ -1,17 +1,16 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIConsoleToolbar.m
// QMUIKit
//
// Created by MoLice on 2019/J/11.
-// Copyright © 2019 QMUI Team. All rights reserved.
//
#import "QMUIConsoleToolbar.h"
@@ -23,8 +22,8 @@
#import "UIImage+QMUI.h"
#import "UIView+QMUI.h"
#import "UIColor+QMUI.h"
-#import "UIControl+QMUI.h"
#import "UIImage+QMUI.h"
+#import "UIControl+QMUI.h"
@interface QMUIConsoleToolbar ()
@@ -88,6 +87,7 @@ - (instancetype)initWithFrame:(CGRect)frame {
[self.searchRightView addSubview:self.searchResultCountLabel];
_searchResultPreviousButton = [[QMUIButton alloc] init];
+ self.searchResultPreviousButton.qmui_preventsRepeatedTouchUpInsideEvent = NO;
[self.searchResultPreviousButton setTitle:@"<" forState:UIControlStateNormal];
self.searchResultPreviousButton.titleLabel.font = UIFontMake(12);
[self.searchResultPreviousButton setTitleColor:self.searchTextField.textColor forState:UIControlStateNormal];
@@ -95,6 +95,7 @@ - (instancetype)initWithFrame:(CGRect)frame {
[self.searchRightView addSubview:self.searchResultPreviousButton];
_searchResultNextButton = [[QMUIButton alloc] init];
+ self.searchResultNextButton.qmui_preventsRepeatedTouchUpInsideEvent = NO;
[self.searchResultNextButton setTitle:@">" forState:UIControlStateNormal];
self.searchResultNextButton.titleLabel.font = UIFontMake(12);
[self.searchResultNextButton setTitleColor:self.searchTextField.textColor forState:UIControlStateNormal];
@@ -112,8 +113,8 @@ - (void)layoutSubviews {
UIEdgeInsets paddings = UIEdgeInsetsMake(8, 8, 8, 8);
- CGFloat x = paddings.left + self.qmui_safeAreaInsets.left;
- CGFloat contentHeight = CGRectGetHeight(self.bounds) - self.qmui_safeAreaInsets.bottom - UIEdgeInsetsGetVerticalValue(paddings);
+ CGFloat x = paddings.left + self.safeAreaInsets.left;
+ CGFloat contentHeight = CGRectGetHeight(self.bounds) - self.safeAreaInsets.bottom - UIEdgeInsetsGetVerticalValue(paddings);
self.levelButton.frame = CGRectMake(x, paddings.top, contentHeight, contentHeight);
x = CGRectGetMaxX(self.levelButton.frame);
@@ -121,7 +122,7 @@ - (void)layoutSubviews {
self.nameButton.frame = CGRectSetX(self.levelButton.frame, CGRectGetMaxX(self.levelButton.frame));
x = CGRectGetMaxX(self.nameButton.frame);
- self.clearButton.frame = CGRectSetX(self.levelButton.frame, CGRectGetWidth(self.bounds) - self.qmui_safeAreaInsets.right - paddings.right - contentHeight);
+ self.clearButton.frame = CGRectSetX(self.levelButton.frame, CGRectGetWidth(self.bounds) - self.safeAreaInsets.right - paddings.right - contentHeight);
CGFloat searchTextFieldMarginHorizontal = 8;
CGFloat searchTextFieldMinX = x + searchTextFieldMarginHorizontal;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h
index 293a05e9..7c264272 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h
@@ -1,9 +1,15 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
//
// QMUIConsoleViewController.h
// QMUIKit
//
// Created by MoLice on 2019/J/11.
-// Copyright © 2019 QMUI Team. All rights reserved.
//
#import
@@ -13,13 +19,13 @@
NS_ASSUME_NONNULL_BEGIN
@class QMUIButton;
-@class QMUITextView;
+@class QMUITableView;
@class QMUIConsoleToolbar;
@interface QMUIConsoleViewController : QMUICommonViewController
@property(nonatomic, strong, readonly) QMUIButton *popoverButton;
-@property(nonatomic, strong, readonly) QMUITextView *textView;
+@property(nonatomic, strong, readonly) QMUITableView *tableView;
@property(nonatomic, strong, readonly) QMUIConsoleToolbar *toolbar;
@property(nonatomic, strong, readonly) NSDateFormatter *dateFormatter;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m
index 13552e60..696aff20 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m
@@ -1,13 +1,22 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
//
// QMUIConsoleViewController.m
// QMUIKit
//
// Created by MoLice on 2019/J/11.
-// Copyright © 2019 QMUI Team. All rights reserved.
//
#import "QMUIConsoleViewController.h"
#import "QMUICore.h"
+#import "QMUITableView.h"
+#import "QMUITableViewCell.h"
+#import "UITableView+QMUICellHeightKeyCache.h"
#import "QMUITextView.h"
#import "QMUITextField.h"
#import "QMUIButton.h"
@@ -17,6 +26,7 @@
#import "UIImage+QMUI.h"
#import "NSObject+QMUI.h"
#import "CAAnimation+QMUI.h"
+#import "NSArray+QMUI.h"
#import "QMUIConsole.h"
#import "QMUIPopupMenuView.h"
@@ -26,12 +36,18 @@ @interface QMUIConsoleLogItem : NSObject
@property(nullable, nonatomic, copy) NSString *name;
@property(nonatomic, copy) NSAttributedString *timeString;
@property(nonatomic, copy) NSAttributedString *logString;
+@property(nonatomic, copy) NSAttributedString *displayString;
+
+@property(nonatomic, copy) NSString *searchingString;
+@property(nonatomic, copy) NSArray *searchResults;
+- (void)updateDisplayStringWithSearchResults:(NSArray *)searchResults;
+- (void)focusSearchResultAtIndex:(NSInteger)index;
@end
@implementation QMUIConsoleLogItem
+ (instancetype)logItemWithLevel:(NSString *)level name:(NSString *)name timeString:(NSString *)timeString logString:(id)logString {
- QMUIConsoleLogItem *logItem = [[QMUIConsoleLogItem alloc] init];
+ QMUIConsoleLogItem *logItem = [[self alloc] init];
logItem.level = level ?: @"Normal";
logItem.name = name ?: @"Default";
@@ -49,21 +65,80 @@ + (instancetype)logItemWithLevel:(NSString *)level name:(NSString *)name timeStr
}
logItem.logString = string;
+ NSMutableAttributedString *displayString = NSMutableAttributedString.new;
+ [displayString appendAttributedString:logItem.timeString];
+ [displayString appendAttributedString:logItem.logString];
+ logItem.displayString = displayString;
return logItem;
}
+- (void)updateDisplayStringWithSearchResults:(NSArray *)searchResults {
+ self.searchResults = searchResults;
+ NSMutableAttributedString *displayString = self.displayString.mutableCopy;
+ [displayString removeAttribute:NSBackgroundColorAttributeName range:NSMakeRange(0, displayString.length)];
+ [searchResults enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ [displayString addAttribute:NSBackgroundColorAttributeName value:[[QMUIConsole appearance].searchResultHighlightedBackgroundColor colorWithAlphaComponent:.4] range:NSMakeRange(self.timeString.length + obj.range.location, obj.range.length)];
+ }];
+ self.displayString = displayString.copy;
+}
+
+- (void)focusSearchResultAtIndex:(NSInteger)index {
+ NSAssert(index < self.searchResults.count, @"尝试聚焦一个超出 searchResults 范围的关键词");
+ [self updateDisplayStringWithSearchResults:self.searchResults];// 重置之前的 focus range
+ NSRange rangeInLogString = self.searchResults[index].range;
+ NSRange range = NSMakeRange(self.timeString.length + rangeInLogString.location, rangeInLogString.length);
+ NSMutableAttributedString *displayString = self.displayString.mutableCopy;
+ [displayString addAttribute:NSBackgroundColorAttributeName value:[QMUIConsole appearance].searchResultHighlightedBackgroundColor range:range];
+ self.displayString = displayString.copy;
+}
+
@end
-@interface QMUIConsoleViewController ()
+@interface QMUIConsoleLogItemCell : QMUITableViewCell
+
+@property(nonatomic, strong) QMUITextView *textView;
+@end
+
+@implementation QMUIConsoleLogItemCell
+
+- (void)didInitializeWithStyle:(UITableViewCellStyle)style {
+ [super didInitializeWithStyle:style];
+ self.backgroundColor = UIColor.clearColor;
+ self.selectionStyle = UITableViewCellSelectionStyleNone;
+
+ self.textView = [[QMUITextView alloc] init];
+ self.textView.textContainerInset = UIEdgeInsetsMake(2, 0, 2, 0);
+ self.textView.backgroundColor = [UIColor clearColor];
+ self.textView.scrollsToTop = NO;
+ self.textView.editable = NO;
+ self.textView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
+ [self.contentView addSubview:self.textView];
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+ return [self.textView sizeThatFits:CGSizeMake(size.width, CGFLOAT_MAX)];
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ self.textView.frame = self.contentView.bounds;
+}
+
+@end
+
+@interface QMUIConsoleViewController ()
@property(nonatomic, strong) UIView *containerView;
@property(nonatomic, strong) QMUIPopupMenuView *levelMenu;
@property(nonatomic, strong) QMUIPopupMenuView *nameMenu;
@property(nonatomic, strong) NSMutableArray *logItems;
+@property(nonatomic, strong) NSArray *showingLogItems;
@property(nonatomic, strong) NSMutableArray *selectedLevels;
@property(nonatomic, strong) NSMutableArray *selectedNames;
-@property(nonatomic, copy) NSArray *searchResults;
+@property(nonatomic, strong) NSRegularExpression *searchRegularExpression;
+@property(nonatomic, assign) NSInteger searchResultsTotalCount;
@property(nonatomic, assign) NSInteger currentHighlightedResultIndex;
+@property(nonatomic, weak) QMUIConsoleLogItem *lastHighlightedItem;
@property(nonatomic, strong) UIPanGestureRecognizer *popoverPanGesture;
@property(nonatomic, strong) UILongPressGestureRecognizer *popoverLongPressGesture;
@@ -74,7 +149,6 @@ @implementation QMUIConsoleViewController
- (void)didInitialize {
[super didInitialize];
- self.automaticallyAdjustsScrollViewInsets = NO;
self.backgroundColor = [QMUIConsole appearance].backgroundColor;
_dateFormatter = [[NSDateFormatter alloc] init];
@@ -83,48 +157,90 @@ - (void)didInitialize {
self.logItems = [[NSMutableArray alloc] init];
self.selectedLevels = [[NSMutableArray alloc] init];
self.selectedNames = [[NSMutableArray alloc] init];
-
- [self loadViewIfNeeded];
+}
+
+- (UIView *)containerView {
+ if (!_containerView) {
+ _containerView = [[UIView alloc] init];
+ _containerView.backgroundColor = self.backgroundColor;
+ _containerView.hidden = YES;
+ }
+ return _containerView;
+}
+
+@synthesize tableView = _tableView;
+- (QMUITableView *)tableView {
+ if (!_tableView) {
+ _tableView = [[QMUITableView alloc] init];
+ _tableView.dataSource = self;
+ _tableView.delegate = self;
+ _tableView.estimatedRowHeight = 44;
+ _tableView.rowHeight = UITableViewAutomaticDimension;
+ _tableView.qmui_cacheCellHeightByKeyAutomatically = YES;
+ _tableView.backgroundColor = nil;
+ _tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
+ _tableView.scrollsToTop = NO;
+ [_tableView registerClass:QMUIConsoleLogItemCell.class forCellReuseIdentifier:@"cell"];
+ _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
+ }
+ return _tableView;
+}
+
+@synthesize toolbar = _toolbar;
+- (QMUIConsoleToolbar *)toolbar {
+ if (!_toolbar) {
+ _toolbar = [[QMUIConsoleToolbar alloc] init];
+ [_toolbar.levelButton addTarget:self action:@selector(handleLevelButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
+ [_toolbar.nameButton addTarget:self action:@selector(handleNameButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
+ [_toolbar.clearButton addTarget:self action:@selector(handleClearButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
+ __weak __typeof(self)weakSelf = self;
+ _toolbar.searchTextField.qmui_keyboardWillChangeFrameNotificationBlock = ^(QMUIKeyboardUserInfo *keyboardUserInfo) {
+ [weakSelf handleKeyboardWillChangeFrame:keyboardUserInfo];
+ };
+ _toolbar.searchTextField.delegate = self;
+ [_toolbar.searchTextField addTarget:self action:@selector(handleSearchTextFieldChanged:) forControlEvents:UIControlEventEditingChanged];
+ [_toolbar.searchResultPreviousButton addTarget:self action:@selector(handleSearchResultPreviousButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
+ [_toolbar.searchResultNextButton addTarget:self action:@selector(handleSearchResultNextButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
+ }
+ return _toolbar;
+}
+
+@synthesize popoverButton = _popoverButton;
+- (QMUIButton *)popoverButton {
+ if (!_popoverButton) {
+ UIImage *popoverImage = [[QMUIHelper imageWithName:@"QMUI_console_logo"] qmui_imageResizedInLimitedSize:CGSizeMake(24, 24)];
+ _popoverButton = [[QMUIButton alloc] qmui_initWithSize:CGSizeMake(32, 32)];
+ [_popoverButton setImage:popoverImage forState:UIControlStateNormal];
+ _popoverButton.adjustsButtonWhenHighlighted = NO;
+ _popoverButton.backgroundColor = [[QMUIConsole appearance].backgroundColor colorWithAlphaComponent:.5];
+ _popoverButton.layer.cornerRadius = CGRectGetHeight(_popoverButton.bounds) / 2;
+ _popoverButton.clipsToBounds = YES;
+ [_popoverButton addTarget:self action:@selector(handlePopoverTouchEvent:) forControlEvents:UIControlEventTouchUpInside];
+
+ self.popoverLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopverLongPressGestureRecognizer:)];
+ [_popoverButton addGestureRecognizer:self.popoverLongPressGesture];
+
+ self.popoverPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopoverPanGestureRecognizer:)];
+ [self.popoverPanGesture requireGestureRecognizerToFail:self.popoverLongPressGesture];
+ [_popoverButton addGestureRecognizer:self.popoverPanGesture];
+ }
+ return _popoverButton;
}
- (void)initSubviews {
[super initSubviews];
-
- self.containerView = [[UIView alloc] init];
- self.containerView.backgroundColor = self.backgroundColor;
- self.containerView.hidden = YES;
[self.view addSubview:self.containerView];
-
- _textView = [[QMUITextView alloc] init];
- self.textView.backgroundColor = [UIColor clearColor];
- self.textView.scrollsToTop = NO;
- self.textView.editable = NO;
- if (@available(iOS 11, *)) {
- self.textView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- }
- [self.containerView addSubview:self.textView];
-
- _toolbar = [[QMUIConsoleToolbar alloc] init];
- [self.toolbar.levelButton addTarget:self action:@selector(handleLevelButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
- [self.toolbar.nameButton addTarget:self action:@selector(handleNameButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
- [self.toolbar.clearButton addTarget:self action:@selector(handleClearButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
- __weak __typeof(self)weakSelf = self;
- self.toolbar.searchTextField.qmui_keyboardWillChangeFrameNotificationBlock = ^(QMUIKeyboardUserInfo *keyboardUserInfo) {
- [weakSelf handleKeyboardWillChangeFrame:keyboardUserInfo];
- };
- self.toolbar.searchTextField.delegate = self;
- [self.toolbar.searchTextField addTarget:self action:@selector(handleSearchTextFieldChanged:) forControlEvents:UIControlEventEditingChanged];
- [self.toolbar.searchResultPreviousButton addTarget:self action:@selector(handleSearchResultPreviousButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
- [self.toolbar.searchResultNextButton addTarget:self action:@selector(handleSearchResultNextButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
+ [self.containerView addSubview:self.tableView];
[self.containerView addSubview:self.toolbar];
- self.levelMenu = [self generatePopupMenuView];
+ __weak __typeof(self)weakSelf = self;
+ self.levelMenu = [self generatePopupMenuViewWithSelectedArray:self.selectedLevels];
self.levelMenu.willHideBlock = ^(BOOL hidesByUserTap, BOOL animated) {
weakSelf.toolbar.levelButton.selected = weakSelf.selectedLevels.count > 0;
};
self.levelMenu.sourceView = self.toolbar.levelButton;
- self.nameMenu = [self generatePopupMenuView];
+ self.nameMenu = [self generatePopupMenuViewWithSelectedArray:self.selectedNames];
self.nameMenu.willHideBlock = ^(BOOL hidesByUserTap, BOOL animated) {
weakSelf.toolbar.nameButton.selected = weakSelf.selectedNames.count > 0;
};
@@ -132,19 +248,6 @@ - (void)initSubviews {
[self updateToolbarButtonState];
- UIImage *popoverImage = [[QMUIHelper imageWithName:@"QMUI_console_logo"] qmui_imageResizedInLimitedSize:CGSizeMake(24, 24)];
- _popoverButton = [[QMUIButton alloc] qmui_initWithSize:CGSizeMake(32, 32)];
- [self.popoverButton setImage:popoverImage forState:UIControlStateNormal];
- self.popoverButton.adjustsButtonWhenHighlighted = NO;
- self.popoverButton.backgroundColor = [[QMUIConsole appearance].backgroundColor colorWithAlphaComponent:.5];
- self.popoverButton.layer.cornerRadius = CGRectGetHeight(self.popoverButton.bounds) / 2;
- self.popoverButton.clipsToBounds = YES;
- [self.popoverButton addTarget:self action:@selector(handlePopoverTouchEvent:) forControlEvents:UIControlEventTouchUpInside];
- self.popoverLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopverLongPressGestureRecognizer:)];
- [self.popoverButton addGestureRecognizer:self.popoverLongPressGesture];
- self.popoverPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopoverPanGestureRecognizer:)];
- [self.popoverPanGesture requireGestureRecognizerToFail:self.popoverLongPressGesture];
- [self.popoverButton addGestureRecognizer:self.popoverPanGesture];
[self.view addSubview:self.popoverButton];
}
@@ -152,7 +255,7 @@ - (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor clearColor];
__weak __typeof(self)weakSelf = self;
- self.view.qmui_hitTestBlock = ^__kindof UIView * _Nonnull(CGPoint point, UIEvent * _Nonnull event, __kindof UIView * _Nonnull originalView) {
+ self.view.qmui_hitTestBlock = ^__kindof UIView * _Nullable(CGPoint point, UIEvent * _Nullable event, __kindof UIView * _Nullable originalView) {
QMUIPopupMenuView *menuView = weakSelf.levelMenu.isShowing ? weakSelf.levelMenu : (weakSelf.nameMenu.isShowing ? weakSelf.nameMenu : nil);
if (menuView && ![originalView isDescendantOfView:menuView]) {
@@ -171,10 +274,10 @@ - (void)viewDidLoad {
}
- (CGRect)safetyPopoverButtonFrame:(CGRect)popoverButtonFrame {
- CGRect safetyBounds = CGRectInsetEdges(self.view.bounds, self.view.qmui_safeAreaInsets);
+ CGRect safetyBounds = CGRectInsetEdges(self.view.bounds, self.view.safeAreaInsets);
if (!CGRectContainsRect(safetyBounds, self.popoverButton.frame)) {
- popoverButtonFrame = CGRectSetX(popoverButtonFrame, MAX(self.view.qmui_safeAreaInsets.left, MIN(CGRectGetMaxX(safetyBounds) - CGRectGetWidth(popoverButtonFrame), CGRectGetMinX(popoverButtonFrame))));
- popoverButtonFrame = CGRectSetY(popoverButtonFrame, MAX(self.view.qmui_safeAreaInsets.top, MIN(CGRectGetMaxY(safetyBounds) - CGRectGetHeight(popoverButtonFrame), CGRectGetMinY(popoverButtonFrame))));
+ popoverButtonFrame = CGRectSetX(popoverButtonFrame, MAX(self.view.safeAreaInsets.left, MIN(CGRectGetMaxX(safetyBounds) - CGRectGetWidth(popoverButtonFrame), CGRectGetMinX(popoverButtonFrame))));
+ popoverButtonFrame = CGRectSetY(popoverButtonFrame, MAX(self.view.safeAreaInsets.top, MIN(CGRectGetMaxY(safetyBounds) - CGRectGetHeight(popoverButtonFrame), CGRectGetMinY(popoverButtonFrame))));
}
return popoverButtonFrame;
}
@@ -186,7 +289,7 @@ - (void)layoutPopoverButton {
if (bindObject) {
popoverButtonOrigin = ((NSValue *)[self.popoverButton qmui_getBoundObjectForKey:@"origin"]).CGPointValue;
} else {
- popoverButtonOrigin = CGPointMake(16 + self.view.qmui_safeAreaInsets.left, CGRectGetHeight(self.view.bounds) * 3.0 / 4.0);
+ popoverButtonOrigin = CGPointMake(16 + self.view.safeAreaInsets.left, CGRectGetHeight(self.view.bounds) * 3.0 / 4.0);
}
self.popoverButton.qmui_frameApplyTransform = [self safetyPopoverButtonFrame:CGRectSetXY(self.popoverButton.frame, popoverButtonOrigin.x, popoverButtonOrigin.y)];
} else {
@@ -204,18 +307,18 @@ - (void)viewDidLayoutSubviews {
CGSize containerViewSize = CGSizeMake(CGRectGetWidth(self.view.bounds), MAX(100, CGRectGetHeight(self.view.bounds) / 3));
self.containerView.qmui_frameApplyTransform = CGRectMake(0, CGRectGetHeight(self.view.bounds) - containerViewSize.height, containerViewSize.width, containerViewSize.height);
- CGFloat toolbarHeight = 44 + self.containerView.qmui_safeAreaInsets.bottom;
+ CGFloat toolbarHeight = 44 + self.containerView.safeAreaInsets.bottom;
self.toolbar.qmui_height = toolbarHeight;
self.toolbar.qmui_width = self.containerView.qmui_width;
self.toolbar.qmui_bottom = self.containerView.qmui_height;
- self.textView.qmui_width = self.containerView.qmui_width;
- self.textView.qmui_height = self.toolbar.qmui_top;
- self.textView.contentInset = UIEdgeInsetsMake(self.textView.qmui_safeAreaInsets.top, self.textView.qmui_safeAreaInsets.left, self.textView.contentInset.bottom, self.textView.qmui_safeAreaInsets.right);
- self.textView.scrollIndicatorInsets = self.textView.contentInset;
+ self.tableView.qmui_width = self.containerView.qmui_width;
+ self.tableView.qmui_height = self.toolbar.qmui_top;
+ self.tableView.contentInset = UIEdgeInsetsMake(self.tableView.safeAreaInsets.top, self.tableView.safeAreaInsets.left, self.tableView.contentInset.bottom, self.tableView.safeAreaInsets.right);
+ self.tableView.scrollIndicatorInsets = self.tableView.contentInset;
[@[self.levelMenu, self.nameMenu] enumerateObjectsUsingBlock:^(QMUIPopupMenuView *menuView, NSUInteger idx, BOOL * _Nonnull stop) {
- menuView.safetyMarginsOfSuperview = UIEdgeInsetsConcat(UIEdgeInsetsMake(2, 2, 2, 2), self.view.qmui_safeAreaInsets);
+ menuView.safetyMarginsOfSuperview = UIEdgeInsetsConcat(UIEdgeInsetsMake(2, 2, 2, 2), self.view.safeAreaInsets);
}];
}
@@ -236,9 +339,11 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor {
- (void)logWithLevel:(NSString *)level name:(NSString *)name logString:(id)logString {
QMUIConsoleLogItem *logItem = [QMUIConsoleLogItem logItemWithLevel:level name:name timeString:[self.dateFormatter stringFromDate:[NSDate new]] logString:logString];
+ [self searchInLogItem:logItem];
[self.logItems addObject:logItem];
- [self updateToolbarButtonState];
- [self printLog];
+ dispatch_async(dispatch_get_main_queue(), ^{// 避免频繁打 log 时卡顿
+ [self printLog];
+ });
}
- (void)log:(id)logString {
@@ -246,24 +351,46 @@ - (void)log:(id)logString {
}
- (void)printLog {
- NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init];
- [self.logItems enumerateObjectsUsingBlock:^(QMUIConsoleLogItem * _Nonnull logItem, NSUInteger idx, BOOL * _Nonnull stop) {
+ self.showingLogItems = [self.logItems qmui_filterWithBlock:^BOOL(QMUIConsoleLogItem * _Nonnull logItem) {
BOOL shouldPrintLevel = !self.selectedLevels.count || [self.selectedLevels containsObject:logItem.level];
BOOL shouldPrintName = !self.selectedNames.count || [self.selectedNames containsObject:logItem.name];
- if (shouldPrintLevel && shouldPrintName) {
- if (string.length > 0) {
- [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:[QMUIConsole appearance].textAttributes]];
- }
-
- [string appendAttributedString:logItem.timeString];
- [string appendAttributedString:logItem.logString];
- }
+ return shouldPrintLevel && shouldPrintName;
}];
- self.textView.attributedText = string;
- if (self.toolbar.searchTextField.text.length > 0) {
- [self handleSearchTextFieldChanged:self.toolbar.searchTextField];
- } else {
- [self.textView qmui_scrollToBottomAnimated:YES];
+ if (_tableView) {
+ [self updateToolbarButtonState];
+
+ [self.tableView reloadData];
+ [self.tableView performBatchUpdates:^{
+ } completion:^(BOOL finished) {
+ NSArray *matchedItems = [self.showingLogItems qmui_filterWithBlock:^BOOL(QMUIConsoleLogItem * _Nonnull item) {
+ return item.searchResults.count > 0;
+ }];
+ NSArray *> *matchedResults = [matchedItems qmui_mapWithBlock:^id _Nonnull(QMUIConsoleLogItem * _Nonnull item, NSInteger index) {
+ return item.searchResults;
+ }];
+ self.searchResultsTotalCount = 0;
+ [matchedResults enumerateObjectsUsingBlock:^(NSArray * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ self.searchResultsTotalCount += obj.count;
+ }];
+
+ BOOL shouldShowCountLabel = self.toolbar.searchTextField.text.length > 0;// 不管有没有结果,只要有搜索文本,就显示结果计数
+ if (shouldShowCountLabel) {
+ self.toolbar.searchTextField.rightViewMode = UITextFieldViewModeAlways;
+ self.toolbar.searchResultPreviousButton.enabled = self.searchResultsTotalCount > 1;
+ self.toolbar.searchResultNextButton.enabled = self.searchResultsTotalCount > 1;
+ } else {
+ self.toolbar.searchTextField.rightViewMode = UITextFieldViewModeNever;
+ }
+ if (self.searchResultsTotalCount == 0) {
+ self.currentHighlightedResultIndex = -1;// < 0 时不会自动滚动,所以需要手动再滚到列表末尾
+ if ([self.tableView numberOfRowsInSection:0] > 0) {
+ NSIndexPath *lastRow = [NSIndexPath indexPathForRow:[self.tableView numberOfRowsInSection:0] - 1 inSection:0];
+ [self.tableView scrollToRowAtIndexPath:lastRow atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
+ }
+ } else {
+ self.currentHighlightedResultIndex = 0;// >= 0 时内部会自动滚动
+ }
+ }];
}
}
@@ -275,9 +402,25 @@ - (void)clear {
self.toolbar.levelButton.selected = NO;
self.toolbar.nameButton.enabled = NO;
self.toolbar.nameButton.selected = NO;
- self.textView.attributedText = nil;
- self.searchResults = nil;
- [self handleSearchTextFieldChanged:self.toolbar.searchTextField];
+ [self printLog];
+}
+
+#pragma mark -
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+ return self.showingLogItems.count;
+}
+
+- (id)qmui_tableView:(UITableView *)tableView cacheKeyForRowAtIndexPath:(NSIndexPath *)indexPath {
+ return self.showingLogItems[indexPath.row].logString.string;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+ QMUIConsoleLogItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
+ QMUIConsoleLogItem *logItem = self.showingLogItems[indexPath.row];
+ cell.textView.attributedText = logItem.displayString.copy;
+ [cell updateCellAppearanceWithIndexPath:indexPath];
+ return cell;
}
#pragma mark - Popover Button
@@ -385,9 +528,7 @@ - (void)handlePopverLongPressGestureRecognizer:(UILongPressGestureRecognizer *)g
scale.qmui_animationDidStopBlock = ^(__kindof CAAnimation *aAnimation, BOOL finished) {
[QMUIConsole hide];
[weakSelf.popoverButton.layer removeAnimationForKey:@"scale"];
- if (@available(iOS 10.0, *)) {
- [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight] impactOccurred];
- }
+ [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] impactOccurred];
};
[self.popoverButton.layer addAnimation:scale forKey:@"scale"];
}
@@ -400,52 +541,52 @@ - (void)updateToolbarButtonState {
self.toolbar.nameButton.enabled = self.logItems.count > 0;
}
-- (QMUIPopupMenuView *)generatePopupMenuView {
+- (QMUIPopupMenuView *)generatePopupMenuViewWithSelectedArray:(NSArray *)selectedArray {
QMUIPopupMenuView *menuView = [[QMUIPopupMenuView alloc] init];
menuView.padding = UIEdgeInsetsMake(3, 6, 3, 6);
menuView.cornerRadius = 3;
menuView.arrowSize = CGSizeMake(8, 4);
menuView.borderWidth = 0;
menuView.itemHeight = 28;
- menuView.itemTitleFont = UIFontMake(12);
- menuView.itemTitleColor = UIColorMake(53, 60, 70);
menuView.maskViewBackgroundColor = nil;
menuView.backgroundColor = UIColorWhite;
- menuView.itemConfigurationHandler = ^(QMUIPopupMenuView *aMenuView, QMUIPopupMenuButtonItem *aItem, NSInteger section, NSInteger index) {
- aItem.highlightedBackgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.15];
- QMUIButton *button = aItem.button;
+ menuView.itemViewConfigurationHandler = ^(QMUIPopupMenuView * _Nonnull aMenuView, __kindof QMUIPopupMenuItem * _Nonnull aItem, QMUIPopupMenuItemView * _Nonnull aItemView, NSInteger section, NSInteger index) {
+ aItemView.button.highlightedBackgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.15];
+ QMUIButton *button = aItemView.button;
+ button.titleLabel.font = UIFontMake(12);
+ button.tintColorAdjustsTitleAndImage = UIColorMake(53, 60, 70);
button.imagePosition = QMUIButtonImagePositionRight;
button.spacingBetweenImageAndTitle = 10;
- UIImage *selectedImage = [UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(12, 9) lineWidth:1 tintColor:aMenuView.itemTitleColor];
+ UIImage *selectedImage = [[UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(12, 9) lineWidth:1 tintColor:UIColor.blackColor] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImage *normalImage = [UIImage qmui_imageWithColor:UIColorClear size:selectedImage.size cornerRadius:0];
[button setImage:normalImage forState:UIControlStateNormal];// 无图像也弄一张空白图,以保证 state 变化时布局不跳动
[button setImage:selectedImage forState:UIControlStateSelected];
[button setImage:selectedImage forState:UIControlStateSelected|UIControlStateHighlighted];
+ button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;
+ button.selected = [selectedArray containsObject:aItem.title];
};
menuView.hidden = YES;
[self.view addSubview:menuView];
return menuView;
}
-- (NSArray *)popupMenuItemsByTitleBlock:(nullable NSString * (^)(QMUIConsoleLogItem *logItem))titleBlock selectedArray:(NSMutableArray *)selectedArray {
+- (NSArray *)popupMenuItemsByTitleBlock:(nullable NSString * (^)(QMUIConsoleLogItem *logItem))titleBlock selectedArray:(NSMutableArray *)selectedArray {
__weak __typeof(self)weakSelf = self;
- NSMutableArray *items = [[NSMutableArray alloc] init];
+ NSMutableArray *items = [[NSMutableArray alloc] init];
NSMutableSet *itemTitles = [[NSMutableSet alloc] init];
[self.logItems enumerateObjectsUsingBlock:^(QMUIConsoleLogItem * _Nonnull logItem, NSUInteger idx, BOOL * _Nonnull stop) {
[itemTitles addObject:titleBlock(logItem)];
}];
[[itemTitles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(description)) ascending:YES]]] enumerateObjectsUsingBlock:^(NSString * _Nonnull title, NSUInteger idx, BOOL * _Nonnull stop) {
- QMUIPopupMenuButtonItem *item = [QMUIPopupMenuButtonItem itemWithImage:nil title:title handler:^(QMUIPopupMenuButtonItem *aItem) {
- aItem.button.selected = !aItem.button.selected;
- if (aItem.button.selected) {
+ QMUIPopupMenuItem *item = [QMUIPopupMenuItem itemWithTitle:title handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, QMUIPopupMenuItemView * _Nonnull aItemView, NSInteger section, NSInteger index) {
+ aItemView.button.selected = !aItemView.button.selected;
+ if (aItemView.button.selected) {
[selectedArray addObject:title];
} else {
[selectedArray removeObject:title];
}
[weakSelf printLog];
}];
- item.button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;
- item.button.selected = [selectedArray containsObject:title];
[items addObject:item];
}];
return items.copy;
@@ -473,64 +614,77 @@ - (void)handleClearButtonEvent:(UIButton *)button {
#pragma mark - Search
+- (void)searchInLogItem:(QMUIConsoleLogItem *)logItem {
+ NSString *searchingText = self.toolbar.searchTextField.text ?: @"";
+ BOOL valueChanged = ![searchingText isEqualToString:logItem.searchingString ?: @""];// UITextField.text 不会为 nil,至少是 @"",为了保证 isEqualToString: 的正确性,这里对 searchingString 也做了 nil -> @"" 的转换
+ if (!valueChanged) return;
+ logItem.searchingString = searchingText;
+ NSArray *matches = [self.searchRegularExpression matchesInString:logItem.logString.string options:NSMatchingReportCompletion range:NSMakeRange(0, logItem.logString.string.length)];
+ [logItem updateDisplayStringWithSearchResults:matches];
+}
+
- (void)handleSearchTextFieldChanged:(QMUITextField *)searchTextField {
if (self.levelMenu.isShowing) [self.levelMenu hideWithAnimated:YES];
if (self.nameMenu.isShowing) [self.nameMenu hideWithAnimated:YES];
- NSError *error = nil;
- NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:searchTextField.text options:NSRegularExpressionCaseInsensitive error:&error];
-
- if (!error) {
- NSString *text = self.textView.text;
- self.searchResults = [regex matchesInString:text options:NSMatchingReportProgress range:NSMakeRange(0, text.length)];
- if (self.searchResults.count) {
- self.currentHighlightedResultIndex = 0;
- self.toolbar.searchResultPreviousButton.enabled = self.searchResults.count > 1;
- self.toolbar.searchResultNextButton.enabled = self.searchResults.count > 1;
-
- self.toolbar.searchResultCountLabel.text = [NSString stringWithFormat:@"%@个结果", @(self.searchResults.count)];
- [self.toolbar setNeedsLayoutSearchResultViews];
- self.toolbar.searchTextField.rightViewMode = UITextFieldViewModeAlways;
- [self updateSearchHighlighted];
- return;
- }
- }
-
- self.currentHighlightedResultIndex = -1;
- self.toolbar.searchTextField.rightViewMode = UITextFieldViewModeNever;
- [self updateSearchHighlighted];
+ self.searchRegularExpression = [NSRegularExpression regularExpressionWithPattern:searchTextField.text options:NSRegularExpressionCaseInsensitive error:nil];
+ [self.logItems enumerateObjectsUsingBlock:^(QMUIConsoleLogItem * _Nonnull logItem, NSUInteger idx, BOOL * _Nonnull stop) {
+ [self searchInLogItem:logItem];
+ }];
+ [self printLog];
}
- (void)handleSearchResultPreviousButtonEvent:(QMUIButton *)button {
if (self.currentHighlightedResultIndex == 0) {
- self.currentHighlightedResultIndex = self.searchResults.count - 1;
+ self.currentHighlightedResultIndex = self.searchResultsTotalCount - 1;
} else {
self.currentHighlightedResultIndex --;
}
- [self updateSearchHighlighted];
}
- (void)handleSearchResultNextButtonEvent:(QMUIButton *)button {
- if (self.currentHighlightedResultIndex == self.searchResults.count - 1) {
+ if (self.currentHighlightedResultIndex == self.searchResultsTotalCount - 1) {
self.currentHighlightedResultIndex = 0;
} else {
self.currentHighlightedResultIndex ++;
}
- [self updateSearchHighlighted];
}
-- (void)updateSearchHighlighted {
- NSMutableAttributedString *attributedText = self.textView.attributedText.mutableCopy;
- [attributedText addAttribute:NSBackgroundColorAttributeName value:[UIColor clearColor] range:NSMakeRange(0, attributedText.length)];
- if (self.currentHighlightedResultIndex >= 0) {
- [attributedText addAttribute:NSBackgroundColorAttributeName value:[QMUIConsole appearance].searchResultHighlightedBackgroundColor range:self.searchResults[self.currentHighlightedResultIndex].range];
- }
-
- self.textView.attributedText = attributedText;
-
- if (self.currentHighlightedResultIndex >= 0) {
- [self.textView scrollRangeToVisible:self.searchResults[self.currentHighlightedResultIndex].range];
+- (void)setCurrentHighlightedResultIndex:(NSInteger)currentHighlightedResultIndex {
+ _currentHighlightedResultIndex = currentHighlightedResultIndex;
+ [self.lastHighlightedItem updateDisplayStringWithSearchResults:self.lastHighlightedItem.searchResults];// clear focus
+ self.toolbar.searchResultCountLabel.text = currentHighlightedResultIndex >= 0 ? [NSString stringWithFormat:@"%@/%@", @(currentHighlightedResultIndex + 1), @(self.searchResultsTotalCount)] : @"0";
+ [self.toolbar setNeedsLayoutSearchResultViews];
+ if (currentHighlightedResultIndex >= 0) {
+
+ NSInteger row = NSNotFound;
+ NSInteger indexInItem = NSNotFound;
+ for (NSInteger i = 0, j = 0; i < self.showingLogItems.count; i++) {
+ if (self.currentHighlightedResultIndex < j + self.showingLogItems[i].searchResults.count) {
+ row = i;
+ indexInItem = self.currentHighlightedResultIndex - j;
+ break;
+ }
+ j += self.showingLogItems[i].searchResults.count;
+ }
+ if (row != NSNotFound) {
+ [self.showingLogItems[row] focusSearchResultAtIndex:indexInItem];
+ [self.tableView reloadData];
+ self.lastHighlightedItem = self.showingLogItems[row];
+ NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
+ BOOL shouldScrollToVisible = ![self.tableView qmui_cellVisibleAtIndexPath:indexPath];
+ if (!shouldScrollToVisible) {
+ // 本来就可视的,可能 cell 比较高,只露出屏幕一半,高亮的那个地方没露出来,这种要手动计算
+ CGRect rect = [self.tableView rectForRowAtIndexPath:indexPath];
+ if (!CGRectContainsRect(self.tableView.bounds, rect)) {
+ shouldScrollToVisible = YES;
+ }
+ }
+ if (shouldScrollToVisible) {
+ [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
+ }
+ }
}
}
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h
index e6d13da6..5b1259ed 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h
@@ -1,17 +1,16 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILog+QMUIConsole.h
// QMUIKit
//
// Created by MoLice on 2019/J/15.
-// Copyright © 2019 QMUI Team. All rights reserved.
//
#import "QMUILog.h"
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m
index 6dc1090e..1eb1526f 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m
@@ -1,17 +1,16 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILog+QMUIConsole.m
// QMUIKit
//
// Created by MoLice on 2019/J/15.
-// Copyright © 2019 QMUI Team. All rights reserved.
//
#import "QMUILog+QMUIConsole.h"
@@ -31,16 +30,23 @@ + (void)load {
originSelectorIMP = (void (*)(id, SEL, const char *, int, const char *, QMUILogItem *))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, file, line, func, logItem);
- // avoid superclass
- if (![selfObject isKindOfClass:originClass]) return;
-
if (!QMUICMIActivated || !ShouldPrintQMUIWarnLogToConsole) return;
if (!logItem.enabled) return;
if (logItem.level != QMUILogLevelWarn) return;
- NSString *funcString = [NSString stringWithFormat:@"%s", func];
- NSString *defaultString = [NSString stringWithFormat:@"%@:%@ | %@", funcString, @(line), logItem];
- [QMUIConsole logWithLevel:logItem.levelDisplayString name:logItem.name logString:defaultString];
+ void (^block)(void) = ^void(void) {
+ NSString *funcString = [NSString stringWithFormat:@"%s", func];
+ NSString *defaultString = [NSString stringWithFormat:@"%@:%@ | %@", funcString, @(line), logItem];
+ [QMUIConsole logWithLevel:logItem.levelDisplayString name:logItem.name logString:defaultString];
+ };
+ if (!NSThread.currentThread.isMainThread) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ block();
+ });
+ } else {
+ block();
+ }
+
};
});
});
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h
index f59f7114..e1396ef7 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIDialogViewController.h
@@ -40,10 +40,18 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, assign) UIEdgeInsets dialogViewMargins UI_APPEARANCE_SELECTOR;
@property(nonatomic, assign) CGFloat maximumContentViewWidth UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic, strong) UIColor *backgroundColor UI_APPEARANCE_SELECTOR;
+
+/// 标题的 tintColor,当没有设置 titleLabelTextColor 和 subTitleLabelTextColor 的情况下,标题和副标题的颜色均会使用 titleTintColor,当 titleLabelTextColor 和 subTitleLabelTextColor 其中任何一个被设置了值时,则 titleTintColor 作为候选项使用(也即谁为 nil 才会用 titleTintColor 顶替,不为 nil 则不会用到 titleTintColor)。
+/// 默认为 nil
@property(nullable, nonatomic, strong) UIColor *titleTintColor UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic, strong) UIFont *titleLabelFont UI_APPEARANCE_SELECTOR;
+
+/// 主标题的文字颜色,当为 nil 时则会使用 titleView 的 tintColor 作为文字颜色
@property(nullable, nonatomic, strong) UIColor *titleLabelTextColor UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic, strong) UIFont *subTitleLabelFont UI_APPEARANCE_SELECTOR;
+
+/// 副标题的文字颜色,当为 nil 时则会使用 titleView 的 tintColor 作为文字颜色
+/// @note 副标题可通过 dialog.titleView.subtitle 来设置
@property(nullable, nonatomic, strong) UIColor *subTitleLabelTextColor UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic, strong) UIColor *headerSeparatorColor UI_APPEARANCE_SELECTOR;
@property(nonatomic, assign) CGFloat headerViewHeight UI_APPEARANCE_SELECTOR;
@@ -55,7 +63,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(nullable, nonatomic, strong) UIColor *footerViewBackgroundColor UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic, strong) UIColor *buttonBackgroundColor UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic, strong) UIColor *buttonHighlightedBackgroundColor UI_APPEARANCE_SELECTOR;
-@property(nullable, nonatomic, strong) NSDictionary *buttonTitleAttributes UI_APPEARANCE_SELECTOR;
+@property(nullable, nonatomic, copy) NSDictionary *buttonTitleAttributes UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic, strong, readonly) UIView *headerView;
@property(nullable, nonatomic, strong, readonly) CALayer *headerViewSeparatorLayer;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m
index 95cba392..94b2c1dc 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIDialogViewController.m
@@ -24,47 +24,41 @@
#import "UITableView+QMUI.h"
#import "NSString+QMUI.h"
#import "UIScrollView+QMUI.h"
+#import "QMUIAppearance.h"
@implementation QMUIDialogViewController (UIAppearance)
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self appearance];
- });
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
}
-static QMUIDialogViewController *dialogViewControllerAppearance;
-+ (nonnull instancetype)appearance {
++ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- if (!dialogViewControllerAppearance) {
- dialogViewControllerAppearance = [[QMUIDialogViewController alloc] init];
- dialogViewControllerAppearance.cornerRadius = 6;
- dialogViewControllerAppearance.dialogViewMargins = UIEdgeInsetsMake(20, 20, 20, 20); // 在 -didInitialize 里会适配 iPhone X 的 safeAreaInsets
- dialogViewControllerAppearance.maximumContentViewWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(dialogViewControllerAppearance.dialogViewMargins);
- dialogViewControllerAppearance.backgroundColor = UIColorWhite;
- dialogViewControllerAppearance.titleTintColor = UIColorBlack;
- dialogViewControllerAppearance.titleLabelFont = UIFontMake(16);
- dialogViewControllerAppearance.titleLabelTextColor = UIColorMake(53, 60, 70);
- dialogViewControllerAppearance.subTitleLabelFont = UIFontMake(12);
- dialogViewControllerAppearance.subTitleLabelTextColor = UIColorMake(133, 140, 150);
-
- dialogViewControllerAppearance.headerSeparatorColor = UIColorMake(222, 224, 226);
- dialogViewControllerAppearance.headerViewHeight = 48;
- dialogViewControllerAppearance.headerViewBackgroundColor = UIColorMake(244, 245, 247);
- dialogViewControllerAppearance.contentViewMargins = UIEdgeInsetsZero;
- dialogViewControllerAppearance.contentViewBackgroundColor = nil;
- dialogViewControllerAppearance.footerSeparatorColor = UIColorMake(222, 224, 226);
- dialogViewControllerAppearance.footerViewHeight = 48;
- dialogViewControllerAppearance.footerViewBackgroundColor = nil;
-
- dialogViewControllerAppearance.buttonBackgroundColor = nil;
- dialogViewControllerAppearance.buttonTitleAttributes = @{NSForegroundColorAttributeName: UIColorBlue};
- dialogViewControllerAppearance.buttonHighlightedBackgroundColor = [UIColorBlue colorWithAlphaComponent:.25];
- }
+ QMUIDialogViewController *dialogViewControllerAppearance = QMUIDialogViewController.appearance;
+ dialogViewControllerAppearance.cornerRadius = 6;
+ dialogViewControllerAppearance.dialogViewMargins = UIEdgeInsetsMake(20, 20, 20, 20); // 在 -didInitialize 里会适配 iPhone X 的 safeAreaInsets
+ dialogViewControllerAppearance.maximumContentViewWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(dialogViewControllerAppearance.dialogViewMargins);
+ dialogViewControllerAppearance.backgroundColor = UIColorWhite;
+ dialogViewControllerAppearance.titleTintColor = nil;
+ dialogViewControllerAppearance.titleLabelFont = UIFontMake(16);
+ dialogViewControllerAppearance.titleLabelTextColor = UIColorMake(53, 60, 70);
+ dialogViewControllerAppearance.subTitleLabelFont = UIFontMake(12);
+ dialogViewControllerAppearance.subTitleLabelTextColor = UIColorMake(133, 140, 150);
+
+ dialogViewControllerAppearance.headerSeparatorColor = UIColorMake(222, 224, 226);
+ dialogViewControllerAppearance.headerViewHeight = 48;
+ dialogViewControllerAppearance.headerViewBackgroundColor = UIColorMake(244, 245, 247);
+ dialogViewControllerAppearance.contentViewMargins = UIEdgeInsetsZero;
+ dialogViewControllerAppearance.contentViewBackgroundColor = nil;
+ dialogViewControllerAppearance.footerSeparatorColor = UIColorMake(222, 224, 226);
+ dialogViewControllerAppearance.footerViewHeight = 48;
+ dialogViewControllerAppearance.footerViewBackgroundColor = nil;
+
+ dialogViewControllerAppearance.buttonBackgroundColor = nil;
+ dialogViewControllerAppearance.buttonTitleAttributes = @{NSForegroundColorAttributeName: UIColorBlue};
+ dialogViewControllerAppearance.buttonHighlightedBackgroundColor = [UIColorBlue colorWithAlphaComponent:.25];
});
- return dialogViewControllerAppearance;
}
@end
@@ -80,28 +74,8 @@ @implementation QMUIDialogViewController
- (void)didInitialize {
[super didInitialize];
- if (dialogViewControllerAppearance) {
- self.cornerRadius = [QMUIDialogViewController appearance].cornerRadius;
- self.dialogViewMargins = UIEdgeInsetsConcat([QMUIDialogViewController appearance].dialogViewMargins, SafeAreaInsetsConstantForDeviceWithNotch);
- self.maximumContentViewWidth = [QMUIDialogViewController appearance].maximumContentViewWidth;
- self.backgroundColor = [QMUIDialogViewController appearance].backgroundColor;
- self.titleTintColor = [QMUIDialogViewController appearance].titleTintColor;
- self.titleLabelFont = [QMUIDialogViewController appearance].titleLabelFont;
- self.titleLabelTextColor = [QMUIDialogViewController appearance].titleLabelTextColor;
- self.subTitleLabelFont = [QMUIDialogViewController appearance].subTitleLabelFont;
- self.subTitleLabelTextColor = [QMUIDialogViewController appearance].subTitleLabelTextColor;
- self.headerSeparatorColor = [QMUIDialogViewController appearance].headerSeparatorColor;
- self.headerViewHeight = [QMUIDialogViewController appearance].headerViewHeight;
- self.headerViewBackgroundColor = [QMUIDialogViewController appearance].headerViewBackgroundColor;
- self.contentViewMargins = [QMUIDialogViewController appearance].contentViewMargins;
- self.contentViewBackgroundColor = [QMUIDialogViewController appearance].contentViewBackgroundColor;
- self.footerSeparatorColor = [QMUIDialogViewController appearance].footerSeparatorColor;
- self.footerViewHeight = [QMUIDialogViewController appearance].footerViewHeight;
- self.footerViewBackgroundColor = [QMUIDialogViewController appearance].footerViewBackgroundColor;
- self.buttonBackgroundColor = [QMUIDialogViewController appearance].buttonBackgroundColor;
- self.buttonTitleAttributes = [QMUIDialogViewController appearance].buttonTitleAttributes;
- self.buttonHighlightedBackgroundColor = [QMUIDialogViewController appearance].buttonHighlightedBackgroundColor;
- }
+
+ [self qmui_applyAppearance];
_contentView = [[UIView alloc] init]; // 特地不使用setter,从而不要影响self.hasCustomContentView的默认值
self.contentView.backgroundColor = self.contentViewBackgroundColor;
@@ -153,7 +127,7 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor {
- (void)setTitleTintColor:(UIColor *)titleTintColor {
_titleTintColor = titleTintColor;
- self.titleView.tintColor = titleTintColor;
+ [self updateTitleViewColor];
}
- (void)setTitleLabelFont:(UIFont *)titleLabelFont {
@@ -164,7 +138,7 @@ - (void)setTitleLabelFont:(UIFont *)titleLabelFont {
- (void)setTitleLabelTextColor:(UIColor *)titleLabelTextColor {
_titleLabelTextColor = titleLabelTextColor;
- self.titleView.titleLabel.textColor = titleLabelTextColor;
+ [self updateTitleViewColor];
}
- (void)setSubTitleLabelFont:(UIFont *)subTitleLabelFont {
@@ -175,7 +149,17 @@ - (void)setSubTitleLabelFont:(UIFont *)subTitleLabelFont {
- (void)setSubTitleLabelTextColor:(UIColor *)subTitleLabelTextColor {
_subTitleLabelTextColor = subTitleLabelTextColor;
- self.titleView.subtitleLabel.textColor = subTitleLabelTextColor;
+ [self updateTitleViewColor];
+}
+
+- (void)updateTitleViewColor {
+ self.titleView.adjustsSubviewsTintColorAutomatically = !self.titleLabelTextColor && !self.subTitleLabelTextColor;
+ if (self.titleView.adjustsSubviewsTintColorAutomatically) {
+ self.titleView.tintColor = self.titleTintColor;// call tintColorDidChange
+ } else {
+ self.titleView.titleLabel.textColor = self.titleLabelTextColor ?: (self.titleTintColor ?: self.titleView.tintColor);
+ self.titleView.subtitleLabel.textColor = self.subTitleLabelTextColor ?: (self.titleTintColor ?: self.titleView.tintColor);
+ }
}
- (void)setHeaderSeparatorColor:(UIColor *)headerSeparatorColor {
@@ -364,7 +348,6 @@ - (void)removeSubmitButton {
- (QMUIButton *)generateButtonWithText:(NSString *)buttonText {
QMUIButton *button = [[QMUIButton alloc] init];
button.titleLabel.font = UIFontBoldMake((IS_320WIDTH_SCREEN) ? 14 : 15);
- button.adjustsTitleTintColorAutomatically = YES;
button.backgroundColor = self.buttonBackgroundColor;
button.highlightedBackgroundColor = self.buttonHighlightedBackgroundColor;
[button setAttributedTitle:[[NSAttributedString alloc] initWithString:buttonText attributes:self.buttonTitleAttributes] forState:UIControlStateNormal];
@@ -438,23 +421,15 @@ - (void)hideModalPresentationComponent {
@implementation QMUIDialogSelectionViewController (UIAppearance)
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self appearance];
- });
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
}
-static QMUIDialogSelectionViewController *dialogSelectionViewControllerAppearance;
-+ (nonnull instancetype)appearance {
++ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- if (!dialogSelectionViewControllerAppearance) {
- dialogSelectionViewControllerAppearance = [[QMUIDialogSelectionViewController alloc] init];
- dialogSelectionViewControllerAppearance.rowHeight = TableViewCellNormalHeight;
- }
+ QMUIDialogSelectionViewController.appearance.rowHeight = TableViewCellNormalHeight;
});
- return dialogSelectionViewControllerAppearance;
}
@end
@@ -471,22 +446,14 @@ @implementation QMUIDialogSelectionViewController
- (void)didInitialize {
[super didInitialize];
- if (dialogSelectionViewControllerAppearance) {
- self.rowHeight = [QMUIDialogSelectionViewController appearance].rowHeight;
- }
-
self.selectedItemIndex = QMUIDialogSelectionViewControllerSelectedItemIndexNone;
self.selectedItemIndexes = [[NSMutableSet alloc] init];
self.tableView = [[QMUITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
- self.tableView.delegate = self;
self.tableView.dataSource = self;
+ self.tableView.delegate = self;
self.tableView.alwaysBounceVertical = NO;
- if (@available(iOS 11, *)) {
- self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- } else {
- self.automaticallyAdjustsScrollViewInsets = NO;
- }
+ self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
// 因为要根据 tableView sizeThatFits: 算出 dialog 的高度,所以禁用 estimated 特性,不然算出来结果不准确
self.tableView.estimatedRowHeight = 0;
@@ -646,31 +613,24 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
@implementation QMUIDialogTextFieldViewController (UIAppearance)
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self appearance];
- });
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
}
-static QMUIDialogTextFieldViewController *dialogTextFieldViewControllerAppearance;
-+ (nonnull instancetype)appearance {
++ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- if (!dialogTextFieldViewControllerAppearance) {
- dialogTextFieldViewControllerAppearance = [[QMUIDialogTextFieldViewController alloc] init];
- dialogTextFieldViewControllerAppearance.textFieldLabelFont = UIFontBoldMake(12);
- dialogTextFieldViewControllerAppearance.textFieldLabelTextColor = UIColorGrayDarken;
- dialogTextFieldViewControllerAppearance.textFieldFont = UIFontMake(17);
- dialogTextFieldViewControllerAppearance.textFieldTextColor = UIColorBlack;
- dialogTextFieldViewControllerAppearance.textFieldSeparatorColor = UIColorSeparator;
- dialogTextFieldViewControllerAppearance.textFieldLabelMargins = UIEdgeInsetsMake(16, 22, -2, 22);
- dialogTextFieldViewControllerAppearance.textFieldMargins = UIEdgeInsetsMake(16, 16, 10, 16);
- dialogTextFieldViewControllerAppearance.textFieldHeight = 25;
- dialogTextFieldViewControllerAppearance.textFieldSeparatorInsets = UIEdgeInsetsMake(0, 0, 16, 0);
- }
+ QMUIDialogTextFieldViewController *dialogTextFieldViewControllerAppearance = QMUIDialogTextFieldViewController.appearance;
+ dialogTextFieldViewControllerAppearance.textFieldLabelFont = UIFontBoldMake(12);
+ dialogTextFieldViewControllerAppearance.textFieldLabelTextColor = UIColorGrayDarken;
+ dialogTextFieldViewControllerAppearance.textFieldFont = UIFontMake(17);
+ dialogTextFieldViewControllerAppearance.textFieldTextColor = UIColorBlack;
+ dialogTextFieldViewControllerAppearance.textFieldSeparatorColor = UIColorSeparator;
+ dialogTextFieldViewControllerAppearance.textFieldLabelMargins = UIEdgeInsetsMake(16, 22, -2, 22);
+ dialogTextFieldViewControllerAppearance.textFieldMargins = UIEdgeInsetsMake(16, 16, 10, 16);
+ dialogTextFieldViewControllerAppearance.textFieldHeight = 25;
+ dialogTextFieldViewControllerAppearance.textFieldSeparatorInsets = UIEdgeInsetsMake(0, 0, 16, 0);
});
- return dialogTextFieldViewControllerAppearance;
}
@end
@@ -688,18 +648,6 @@ @implementation QMUIDialogTextFieldViewController
- (void)didInitialize {
[super didInitialize];
- if (dialogTextFieldViewControllerAppearance) {
- self.textFieldLabelFont = [QMUIDialogTextFieldViewController appearance].textFieldLabelFont;
- self.textFieldLabelTextColor = [QMUIDialogTextFieldViewController appearance].textFieldLabelTextColor;
- self.textFieldFont = [QMUIDialogTextFieldViewController appearance].textFieldFont;
- self.textFieldTextColor = [QMUIDialogTextFieldViewController appearance].textFieldTextColor;
- self.textFieldSeparatorColor = [QMUIDialogTextFieldViewController appearance].textFieldSeparatorColor;
- self.textFieldLabelMargins = [QMUIDialogTextFieldViewController appearance].textFieldLabelMargins;
- self.textFieldMargins = [QMUIDialogTextFieldViewController appearance].textFieldMargins;
- self.textFieldHeight = [QMUIDialogTextFieldViewController appearance].textFieldHeight;
- self.textFieldSeparatorInsets = [QMUIDialogTextFieldViewController appearance].textFieldSeparatorInsets;
- }
-
self.mutableTitleLabels = [[NSMutableArray alloc] init];
self.mutableTextFields = [[NSMutableArray alloc] init];
self.mutableSeparatorLayers = [[NSMutableArray alloc] init];
@@ -710,11 +658,7 @@ - (void)didInitialize {
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.scrollsToTop = NO;
self.scrollView.clipsToBounds = YES;
- if (@available(iOS 11, *)) {
- self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- } else {
- self.automaticallyAdjustsScrollViewInsets = NO;
- }
+ self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
self.contentView = self.scrollView;
self.scrollView.backgroundColor = self.contentViewBackgroundColor;
}
@@ -771,7 +715,7 @@ - (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// 全部基于 contentView 布局即可
- NSAssert(self.mutableTitleLabels.count == self.mutableTextFields.count && self.mutableTextFields.count == self.mutableSeparatorLayers.count, @"%@ 里的标题、输入框、分隔线数量不匹配", NSStringFromClass(self.class));
+ QMUIAssert(self.mutableTitleLabels.count == self.mutableTextFields.count && self.mutableTextFields.count == self.mutableSeparatorLayers.count, NSStringFromClass(self.class), @"标题、输入框、分隔线的数量不匹配");
CGFloat minY = 0;
@@ -977,7 +921,7 @@ - (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresen
}
}
- CGFloat contentHeight = textFieldLabelHeight + textFieldHeight + separatorHeight + UIEdgeInsetsGetVerticalValue(self.scrollView.qmui_contentInset);
+ CGFloat contentHeight = textFieldLabelHeight + textFieldHeight + separatorHeight + UIEdgeInsetsGetVerticalValue(self.scrollView.adjustedContentInset);
CGFloat contentViewVerticalMargin = UIEdgeInsetsGetVerticalValue(self.contentViewMargins);
BOOL isFooterViewShowing = self.footerView && !self.footerView.hidden;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h
index 87005d3c..f9e3b0ae 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIEmotionInputManager.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m
index 323d5a96..b594a698 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIEmotionInputManager.m
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h
index d87cb681..7bf3e296 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIEmotionView.h
@@ -47,7 +47,7 @@
* 表情控件,支持任意表情的展示,每个表情以相同的大小显示。
*
* 使用方式:
- *
+ *
* - 通过`initWithFrame:`初始化,如果面板高度不变,建议在init时就设置好,若最终布局以父类的`layoutSubviews`为准,则也可通过`init`方法初始化,再在`layoutSubviews`里计算布局
* - 通过调整`paddingInPage`、`emotionSize`等变量来自定义UI
* - 通过`emotions`设置要展示的表情
@@ -75,6 +75,15 @@
/// 用于展示表情面板的横向滚动collectionView,布局撑满整个控件
@property(nonatomic, strong, readonly) UICollectionView *collectionView;
+/// 用于展示表情面板的竖向滚动的 scrollView,布局撑满整个控件
+@property(nonatomic, strong, readonly) UIScrollView *scrollView;
+
+/// 竖向滚动,默认为 NO
+@property(nonatomic, assign) BOOL verticalAlignment UI_APPEARANCE_SELECTOR;
+
+/// 表情与表情之间的垂直间距,默认为10,仅在 verticalAlignment 为 YES 时生效,当 verticalAlignment 为 N0 时,表情的垂直间距由 numberOfRowsPerPage 决定
+@property(nonatomic, assign) CGFloat emotionVerticalSpacing UI_APPEARANCE_SELECTOR;
+
/// 用于横向按页滚动的collectionViewLayout
@property(nonatomic, strong, readonly) UICollectionViewFlowLayout *collectionViewLayout;
@@ -84,6 +93,9 @@
/// 控件右下角的发送按钮
@property(nonatomic, strong, readonly) QMUIButton *sendButton;
+/// 控件右下角的删除按钮
+@property(nonatomic, strong, readonly) QMUIButton *deleteButton;
+
/// 每一页表情的上下左右padding,默认为{18, 18, 65, 18}
@property(nonatomic, assign) UIEdgeInsets paddingInPage UI_APPEARANCE_SELECTOR;
@@ -102,6 +114,15 @@
/// 表情面板右下角的删除按钮的图片,默认为`[QMUIHelper imageWithName:@"QMUI_emotion_delete"]`
@property(nonatomic, strong) UIImage *deleteButtonImage UI_APPEARANCE_SELECTOR;
+/// 删除按钮的背景色,默认为 nil
+@property(nonatomic, strong) UIColor *deleteButtonBackgroundColor UI_APPEARANCE_SELECTOR;
+
+/// 删除按钮位置的 (x,y) 的偏移,默认为 CGPointZero
+@property(nonatomic, assign) CGPoint deleteButtonOffset UI_APPEARANCE_SELECTOR;
+
+/// 删除按钮的圆角大小,默认为4
+@property(nonatomic, assign) CGFloat deleteButtonCornerRadius UI_APPEARANCE_SELECTOR;
+
/// 发送按钮的文字样式,默认为{NSFontAttributeName: UIFontMake(15), NSForegroundColorAttributeName: UIColorWhite}
@property(nonatomic, strong) NSDictionary *sendButtonTitleAttributes UI_APPEARANCE_SELECTOR;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m
index 932dbc86..e73bfd7b 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIEmotionView.m
@@ -17,6 +17,7 @@
#import "QMUICore.h"
#import "QMUIButton.h"
#import "UIView+QMUI.h"
+#import "CALayer+QMUI.h"
#import "UIScrollView+QMUI.h"
#import "UIControl+QMUI.h"
#import "UIImage+QMUI.h"
@@ -25,12 +26,19 @@
@implementation QMUIEmotion
+ (instancetype)emotionWithIdentifier:(NSString *)identifier displayName:(NSString *)displayName {
- QMUIEmotion *emotion = [[QMUIEmotion alloc] init];
+ QMUIEmotion *emotion = [[self alloc] init];
emotion.identifier = identifier;
emotion.displayName = displayName;
return emotion;
}
+- (BOOL)isEqual:(id)object {
+ if (!object) return NO;
+ if (self == object) return YES;
+ if (![object isKindOfClass:[self class]]) return NO;
+ return [self.identifier isEqualToString:((QMUIEmotion *)object).identifier];
+}
+
- (NSString *)description {
return [NSString stringWithFormat:@"%@, identifier: %@, displayName: %@", [super description], self.identifier, self.displayName];
}
@@ -43,8 +51,7 @@ @protocol QMUIEmotionPageViewDelegate
@optional
- (void)emotionPageView:(QMUIEmotionPageView *)emotionPageView didSelectEmotion:(QMUIEmotion *)emotion atIndex:(NSInteger)index;
-- (void)didSelectDeleteButtonInEmotionPageView:(QMUIEmotionPageView *)emotionPageView;
-
+- (void)emotionPageViewDidLayoutEmotions:(QMUIEmotionPageView *)emotionPageView;
@end
/// 表情面板每一页的cell,在drawRect里将所有表情绘制上去,同时自带一个末尾的删除按钮
@@ -56,7 +63,16 @@ @interface QMUIEmotionPageView : UICollectionViewCell
@property(nonatomic, strong) UIView *emotionSelectedBackgroundView;
/// 表情面板右下角的删除按钮
-@property(nonatomic, strong) QMUIButton *deleteButton;
+@property(nonatomic, weak) QMUIButton *deleteButton;
+
+/// 表情面板右下角的删除按的截图,因为在 CollectionView 滑动的过程中可能会出现 2 个 deleteButton,但是真实的 deleteButton 只能有一个,所以用截图来过渡
+@property(nonatomic, strong) UIView *deleteButtonSnapView;
+
+/// 删除按钮位置的 (x,y) 的偏移
+@property(nonatomic, assign) CGPoint deleteButtonOffset;
+
+/// 所有表情的 Layer
+@property(nonatomic, strong) NSMutableArray *emotionLayers;
/// 分配给当前pageView的所有表情
@property(nonatomic, copy) NSArray *emotions;
@@ -84,6 +100,11 @@ @interface QMUIEmotionPageView : UICollectionViewCell
/// debug模式会把表情的绘制矩形显示出来
@property(nonatomic, assign) BOOL debug;
+
+@property(nonatomic, assign, readonly) BOOL needsLayoutEmotions;
+
+@property(nonatomic, assign) CGRect previousLayoutFrame;
+
@end
@implementation QMUIEmotionPageView
@@ -100,12 +121,6 @@ - (instancetype)initWithFrame:(CGRect)frame {
self.emotionSelectedBackgroundView.alpha = 0;
[self addSubview:self.emotionSelectedBackgroundView];
- self.deleteButton = [[QMUIButton alloc] init];
- self.deleteButton.adjustsButtonWhenHighlighted = NO;// 去掉QMUIButton默认的高亮动画,从而加快连续快速点击的响应速度
- self.deleteButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES;
- [self.deleteButton addTarget:self action:@selector(handleDeleteButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
- [self addSubview:self.deleteButton];
-
self.emotionHittingRects = [[NSMutableArray alloc] init];
self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGestureRecognizer:)];
[self addGestureRecognizer:self.tapGestureRecognizer];
@@ -113,58 +128,101 @@ - (instancetype)initWithFrame:(CGRect)frame {
return self;
}
+- (CGRect)frameForDeleteButton:(__kindof UIView *)deleteButton {
+ return CGRectSetXY(deleteButton.frame, CGRectGetWidth(self.bounds) - self.padding.right - CGRectGetWidth(deleteButton.frame) - (self.emotionSize.width - CGRectGetWidth(deleteButton.frame)) / 2.0 + self.deleteButtonOffset.x, CGRectGetHeight(self.bounds) - self.padding.bottom - CGRectGetHeight(deleteButton.frame) - (self.emotionSize.height - CGRectGetHeight(deleteButton.frame)) / 2.0 + self.deleteButtonOffset.y);
+}
+
- (void)layoutSubviews {
[super layoutSubviews];
- // 删除按钮必定布局到最后一个表情的位置,且与表情上下左右居中
- [self.deleteButton sizeToFit];
- self.deleteButton.frame = CGRectSetXY(self.deleteButton.frame, CGRectGetWidth(self.bounds) - self.padding.right - CGRectGetWidth(self.deleteButton.frame) - (self.emotionSize.width - CGRectGetWidth(self.deleteButton.frame)) / 2.0, CGRectGetHeight(self.bounds) - self.padding.bottom - CGRectGetHeight(self.deleteButton.frame) - (self.emotionSize.height - CGRectGetHeight(self.deleteButton.frame)) / 2.0);
+
+ if (self.deleteButton.superview == self) {
+ // 删除按钮必定布局到最后一个表情的位置,且与表情上下左右居中
+ [self.deleteButton sizeToFit];
+ self.deleteButton.frame = [self frameForDeleteButton:self.deleteButton];
+ }
+ if (self.deleteButtonSnapView) {
+ self.deleteButtonSnapView.frame = [self frameForDeleteButton:self.deleteButtonSnapView];
+ }
+ BOOL isSizeChanged = !CGSizeEqualToSize(self.previousLayoutFrame.size, self.frame.size);
+ self.previousLayoutFrame = self.frame;
+ if (isSizeChanged) {
+ [self setNeedsLayoutEmotions];
+ }
+ [self layoutEmotionsIfNeeded];
+}
+
+- (void)willRemoveSubview:(UIView *)subview {
+ if (subview == self.deleteButton) {
+ self.deleteButtonSnapView = [self.deleteButton snapshotViewAfterScreenUpdates:NO];
+ [self addSubview:self.deleteButtonSnapView];
+ }
+}
+
+- (void)setNeedsLayoutEmotions {
+ _needsLayoutEmotions = YES;
+}
+
+- (void)setEmotions:(NSArray *)emotions {
+ if ([_emotions isEqualToArray:emotions]) return;
+ _emotions = emotions;
+ [self setNeedsLayoutEmotions];
+ [self setNeedsLayout];
}
-- (void)drawRect:(CGRect)rect {
+- (void)layoutEmotionsIfNeeded {
+ if (!self.needsLayoutEmotions) return;
+ _needsLayoutEmotions = NO;
[self.emotionHittingRects removeAllObjects];
CGSize contentSize = CGRectInsetEdges(self.bounds, self.padding).size;
NSInteger emotionCountPerRow = (contentSize.width + self.minimumEmotionHorizontalSpacing) / (self.emotionSize.width + self.minimumEmotionHorizontalSpacing);
CGFloat emotionHorizontalSpacing = flat((contentSize.width - emotionCountPerRow * self.emotionSize.width) / (emotionCountPerRow - 1));
CGFloat emotionVerticalSpacing = flat((contentSize.height - self.numberOfRows * self.emotionSize.height) / (self.numberOfRows - 1));
-
CGPoint emotionOrigin = CGPointZero;
- for (NSInteger i = 0, l = self.emotions.count; i < l; i++) {
+ NSInteger emotionCount = self.emotions.count;
+ if (!self.emotionLayers) {
+ self.emotionLayers = [NSMutableArray arrayWithCapacity:emotionCount];
+ }
+ for (NSInteger i = 0; i < emotionCount; i++) {
+ CALayer *emotionlayer = nil;
+ if (i < self.emotionLayers.count) {
+ emotionlayer = self.emotionLayers[i];
+ } else {
+ emotionlayer = [CALayer layer];
+ emotionlayer.contentsScale = ScreenScale;
+ [self.emotionLayers addObject:emotionlayer];
+ [self.layer addSublayer:emotionlayer];
+ }
+
+ emotionlayer.contents = (__bridge id)(self.emotions[i].image.CGImage);
NSInteger row = i / emotionCountPerRow;
emotionOrigin.x = self.padding.left + (self.emotionSize.width + emotionHorizontalSpacing) * (i % emotionCountPerRow);
emotionOrigin.y = self.padding.top + (self.emotionSize.height + emotionVerticalSpacing) * row;
- QMUIEmotion *emotion = self.emotions[i];
CGRect emotionRect = CGRectMake(emotionOrigin.x, emotionOrigin.y, self.emotionSize.width, self.emotionSize.height);
CGRect emotionHittingRect = CGRectInsetEdges(emotionRect, self.emotionSelectedBackgroundExtension);
[self.emotionHittingRects addObject:[NSValue valueWithCGRect:emotionHittingRect]];
- [self drawImage:emotion.image inRect:emotionRect];
+ emotionlayer.frame = emotionRect;
+ emotionlayer.hidden = NO;
}
-}
-
-- (void)drawImage:(UIImage *)image inRect:(CGRect)contextRect {
- CGSize imageSize = image.size;
- CGFloat horizontalRatio = CGRectGetWidth(contextRect) / imageSize.width;
- CGFloat verticalRatio = CGRectGetHeight(contextRect) / imageSize.height;
- // 表情图片按UIViewContentModeScaleAspectFit的方式来绘制
- CGFloat ratio = fmin(horizontalRatio, verticalRatio);
- CGRect drawingRect = CGRectZero;
- drawingRect.size.width = imageSize.width * ratio;
- drawingRect.size.height = imageSize.height * ratio;
- drawingRect = CGRectSetXY(drawingRect, CGRectGetMinXHorizontallyCenter(contextRect, drawingRect), CGRectGetMinYVerticallyCenter(contextRect, drawingRect));
- if (self.debug) {
- CGContextRef context = UIGraphicsGetCurrentContext();
- CGContextSetLineWidth(context, PixelOne);
- CGContextSetStrokeColorWithColor(context, UIColorTestRed.CGColor);
- CGContextStrokeRect(context, CGRectInset(contextRect, PixelOne / 2.0, PixelOne / 2.0));
+
+ if (self.emotionLayers.count > emotionCount) {
+ for (NSInteger i = self.emotionLayers.count - emotionCount - 1; i < self.emotionLayers.count; i++) {
+ self.emotionLayers[i].hidden = YES;
+ }
+ }
+ if ([self.delegate respondsToSelector:@selector(emotionPageViewDidLayoutEmotions:)]) {
+ [self.delegate emotionPageViewDidLayoutEmotions:self];
}
- [image drawInRect:drawingRect];
}
+
- (void)handleTapGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer {
CGPoint location = [gestureRecognizer locationInView:self];
for (NSInteger i = 0; i < self.emotionHittingRects.count; i ++) {
CGRect rect = [self.emotionHittingRects[i] CGRectValue];
if (CGRectContainsPoint(rect, location)) {
+ CALayer *layer = self.emotionLayers[i];
+ if (layer.opacity < 0.2) return;
QMUIEmotion *emotion = self.emotions[i];
self.emotionSelectedBackgroundView.frame = rect;
[UIView animateWithDuration:.08 animations:^{
@@ -185,16 +243,98 @@ - (void)handleTapGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer {
}
}
-- (void)handleDeleteButtonEvent:(QMUIButton *)deleteButton {
- if ([self.delegate respondsToSelector:@selector(didSelectDeleteButtonInEmotionPageView:)]) {
- [self.delegate didSelectDeleteButtonInEmotionPageView:self];
+- (CGSize)verticalSizeThatFits:(CGSize)size emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing {
+ CGSize contentSize = CGRectInsetEdges(CGRectMakeWithSize(size), self.padding).size;
+ NSInteger emotionCountPerRow = (contentSize.width + self.minimumEmotionHorizontalSpacing) / (self.emotionSize.width + self.minimumEmotionHorizontalSpacing);
+ NSInteger row = ceil(self.emotions.count / (emotionCountPerRow * 1.0));
+ CGFloat height = (self.emotionSize.height + emotionVerticalSpacing) * row - emotionVerticalSpacing + UIEdgeInsetsGetVerticalValue(self.padding);
+ return CGSizeMake(size.width, height);
+}
+
+- (void)updateDeleteButton:(QMUIButton *)deleteButton {
+ _deleteButton = deleteButton;
+ if (self.deleteButtonSnapView) {
+ [self.deleteButtonSnapView removeFromSuperview];
+ self.deleteButtonSnapView = nil;
}
+ [self addSubview:deleteButton];
+}
+
+- (void)setDeleteButtonOffset:(CGPoint)deleteButtonOffset {
+ _deleteButtonOffset = deleteButtonOffset;
+ [self setNeedsLayout];
}
+
@end
-@interface QMUIEmotionView ()
+@interface QMUIEmotionVerticalScrollView : UIScrollView
+@property(nonatomic, strong) QMUIEmotionPageView *pageView;
+@end
+@implementation QMUIEmotionVerticalScrollView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ _pageView = [[QMUIEmotionPageView alloc] init];
+ self.pageView.deleteButton.hidden = YES;
+ [self addSubview:self.pageView];
+ }
+ return self;
+}
+
+- (void)setEmotions:(NSArray *)emotions
+ emotionSize:(CGSize)emotionSize
+ minimumEmotionHorizontalSpacing:(CGFloat)minimumEmotionHorizontalSpacing
+ emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing
+ emotionSelectedBackgroundExtension:(UIEdgeInsets)emotionSelectedBackgroundExtension
+ paddingInPage:(UIEdgeInsets)paddingInPage {
+ QMUIEmotionPageView *pageView = self.pageView;
+ pageView.emotions = emotions;
+ pageView.padding = paddingInPage;
+ CGSize contentSize = CGSizeMake(self.bounds.size.width - UIEdgeInsetsGetHorizontalValue(paddingInPage), self.bounds.size.height - UIEdgeInsetsGetVerticalValue(paddingInPage));
+ NSInteger emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing);
+ pageView.numberOfRows = ceil(emotions.count / (CGFloat)emotionCountPerRow);
+ pageView.emotionSize =emotionSize;
+ pageView.emotionSelectedBackgroundExtension = emotionSelectedBackgroundExtension;
+ pageView.minimumEmotionHorizontalSpacing = minimumEmotionHorizontalSpacing;
+ [pageView setNeedsLayout];
+ CGSize size = [pageView verticalSizeThatFits:self.bounds.size emotionVerticalSpacing:emotionVerticalSpacing];
+ self.pageView.frame = CGRectMakeWithSize(size);
+ self.contentSize = size;
+}
+
+- (void)adjustEmotionsAlphaWithFloatingRect:(CGRect)floatingRect {
+ CGSize contentSize = CGSizeMake(self.contentSize.width - UIEdgeInsetsGetHorizontalValue(self.pageView.padding), self.contentSize.height - UIEdgeInsetsGetVerticalValue(self.pageView.padding));
+ NSInteger emotionCountPerRow = (contentSize.width + self.pageView.minimumEmotionHorizontalSpacing) / (self.pageView.emotionSize.width + self.pageView.minimumEmotionHorizontalSpacing);
+ CGFloat emotionVerticalSpacing = flat((contentSize.height - self.pageView.numberOfRows * self.pageView.emotionSize.height) / (self.pageView.numberOfRows - 1));
+ NSInteger columnIndexLeft = ceil((floatingRect.origin.x - self.pageView.padding.left) / (self.pageView.emotionSize.width + self.pageView.minimumEmotionHorizontalSpacing)) - 1;
+ NSInteger columnIndexRight = emotionCountPerRow - 1;
+ CGFloat rowIndexTop = ((floatingRect.origin.y - self.pageView.padding.top) / (self.pageView.emotionSize.height + emotionVerticalSpacing)) - 1;
+ for (NSInteger i = 0; i < self.pageView.emotionLayers.count; i++) {
+ NSInteger row = (i / emotionCountPerRow);
+ NSInteger column = (i % emotionCountPerRow);
+ [CALayer qmui_performWithoutAnimation:^{
+ if (column >= columnIndexLeft && column <= columnIndexRight && row > rowIndexTop) {
+ if (row == ceil(rowIndexTop)) {
+ CGFloat intersectAreaHeight = floatingRect.origin.y - self.pageView.emotionLayers[i].frame.origin.y;
+ CGFloat percent = intersectAreaHeight / self.pageView.emotionSize.height;
+ self.pageView.emotionLayers[i].opacity = percent * percent;
+ } else {
+ self.pageView.emotionLayers[i].opacity = 0;
+ }
+ } else {
+ self.pageView.emotionLayers[i].opacity = 1.0f;
+ }
+ }];
+ }
+}
+
+@end
+
+@interface QMUIEmotionView ()
+/// 用于展示表情面板的竖向滚动 scrollView,布局撑满整个控件
+@property(nonatomic, strong, readonly) QMUIEmotionVerticalScrollView *verticalScrollView;
@property(nonatomic, strong) NSMutableArray *> *pagedEmotions;
@property(nonatomic, assign) BOOL debug;
@end
@@ -216,6 +356,17 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {
return self;
}
+- (void)setVerticalAlignment:(BOOL)verticalAlignment {
+ _verticalAlignment = verticalAlignment;
+ self.collectionView.hidden = verticalAlignment;
+ self.pageControl.hidden = verticalAlignment;
+ self.verticalScrollView.hidden = !verticalAlignment;
+ if (!verticalAlignment && self.deleteButton.superview) {
+ [self.deleteButton removeFromSuperview];
+ }
+ [self setNeedsLayout];
+}
+
- (void)didInitializedWithFrame:(CGRect)frame {
self.debug = NO;
@@ -227,7 +378,8 @@ - (void)didInitializedWithFrame:(CGRect)frame {
self.collectionViewLayout.minimumInteritemSpacing = 0;
self.collectionViewLayout.sectionInset = UIEdgeInsetsZero;
- _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(self.qmui_safeAreaInsets.left, self.qmui_safeAreaInsets.top, CGRectGetWidth(frame) - UIEdgeInsetsGetHorizontalValue(self.qmui_safeAreaInsets), CGRectGetHeight(frame) - UIEdgeInsetsGetVerticalValue(self.qmui_safeAreaInsets)) collectionViewLayout:self.collectionViewLayout];
+ _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(self.safeAreaInsets.left, self.safeAreaInsets.top, CGRectGetWidth(frame) - UIEdgeInsetsGetHorizontalValue(self.safeAreaInsets), CGRectGetHeight(frame) - UIEdgeInsetsGetVerticalValue(self.safeAreaInsets)) collectionViewLayout:self.collectionViewLayout];
+ self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
self.collectionView.backgroundColor = UIColorClear;
self.collectionView.scrollsToTop = NO;
self.collectionView.pagingEnabled = YES;
@@ -237,6 +389,12 @@ - (void)didInitializedWithFrame:(CGRect)frame {
[self.collectionView registerClass:[QMUIEmotionPageView class] forCellWithReuseIdentifier:@"page"];
[self addSubview:self.collectionView];
+ _verticalScrollView = [[QMUIEmotionVerticalScrollView alloc] init];
+ self.verticalScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
+ _verticalScrollView.delegate = self;
+ _verticalScrollView.hidden = YES;
+ [self addSubview:self.verticalScrollView];
+
_pageControl = [[UIPageControl alloc] init];
[self.pageControl addTarget:self action:@selector(handlePageControlEvent:) forControlEvents:UIControlEventValueChanged];
[self addSubview:self.pageControl];
@@ -244,33 +402,66 @@ - (void)didInitializedWithFrame:(CGRect)frame {
_sendButton = [[QMUIButton alloc] init];
[self.sendButton setTitle:@"发送" forState:UIControlStateNormal];
self.sendButton.contentEdgeInsets = UIEdgeInsetsMake(5, 17, 5, 17);
- [self.sendButton sizeToFit];
[self addSubview:self.sendButton];
+
+ _deleteButton = [[QMUIButton alloc] init];
+ self.deleteButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES;
+ __weak __typeof(self)weakSelf = self;
+ self.deleteButton.qmui_tapBlock = ^(__kindof UIControl *sender) {
+ __strong __typeof(weakSelf)strongSelf = weakSelf;
+ if (strongSelf.didSelectDeleteButtonBlock) {
+ strongSelf.didSelectDeleteButtonBlock();
+ }
+ };
}
- (void)setEmotions:(NSArray *)emotions {
_emotions = emotions;
- [self pageEmotions];
+ if (self.verticalAlignment) {
+ [self setNeedsLayout];
+ } else {
+ [self pageEmotions];
+ }
}
- (void)layoutSubviews {
[super layoutSubviews];
- CGRect collectionViewFrame = CGRectInsetEdges(self.bounds, self.qmui_safeAreaInsets);
- BOOL collectionViewSizeChanged = !CGSizeEqualToSize(collectionViewFrame.size, self.collectionView.bounds.size);
- self.collectionViewLayout.itemSize = collectionViewFrame.size;// 先更新 itemSize 再设置 collectionView.frame,否则会触发系统的 UICollectionViewFlowLayoutBreakForInvalidSizes 断点
- self.collectionView.frame = collectionViewFrame;
- if (collectionViewSizeChanged) {
- [self pageEmotions];
+ [self.sendButton sizeToFit];
+ self.sendButton.qmui_right = self.qmui_width - self.safeAreaInsets.right - self.sendButtonMargins.right;
+ self.sendButton.qmui_bottom = self.qmui_height - self.safeAreaInsets.bottom - self.sendButtonMargins.bottom;
+ if (self.verticalAlignment) {
+ CGRect verticalScrollViewFrame = CGRectInsetEdges(self.bounds, UIEdgeInsetsSetBottom(self.safeAreaInsets, 0));
+ self.verticalScrollView.frame = verticalScrollViewFrame;
+ [self.verticalScrollView setEmotions:self.emotions
+ emotionSize:self.emotionSize
+ minimumEmotionHorizontalSpacing:self.minimumEmotionHorizontalSpacing
+ emotionVerticalSpacing:self.emotionVerticalSpacing
+ emotionSelectedBackgroundExtension:self.emotionSelectedBackgroundExtension
+ paddingInPage:UIEdgeInsetsSetBottom(self.paddingInPage, self.paddingInPage.bottom + self.safeAreaInsets.bottom)];
+ self.verticalScrollView.pageView.delegate = self;
+ [self addSubview:self.deleteButton];
+ [self.deleteButton setImage:self.deleteButtonImage forState:UIControlStateNormal];
+ [self.deleteButton setImage:[self.deleteButtonImage qmui_imageWithAlpha:ButtonHighlightedAlpha] forState:UIControlStateHighlighted];
+ self.deleteButton.bounds = CGRectMakeWithSize(CGSizeMake([self.deleteButton sizeThatFits:CGSizeZero].width, self.sendButton.qmui_height));
+ static CGFloat spacingBetweenDeleteButtonAndSendButton = 4.0f;
+ self.deleteButton.qmui_right = self.sendButton.qmui_left - spacingBetweenDeleteButtonAndSendButton + self.deleteButtonOffset.x;
+ self.deleteButton.qmui_top = CGRectGetMinYVerticallyCenter(self.sendButton.frame, self.deleteButton.frame) + self.deleteButtonOffset.y;
+
+ } else {
+ CGRect collectionViewFrame = CGRectInsetEdges(self.bounds, self.safeAreaInsets);
+ BOOL collectionViewSizeChanged = !CGSizeEqualToSize(collectionViewFrame.size, self.collectionView.bounds.size);
+ self.collectionViewLayout.itemSize = collectionViewFrame.size;// 先更新 itemSize 再设置 collectionView.frame,否则会触发系统的 UICollectionViewFlowLayoutBreakForInvalidSizes 断点
+ self.collectionView.frame = collectionViewFrame;
+
+ if (collectionViewSizeChanged) {
+ [self pageEmotions];
+ }
+ CGFloat pageControlHeight = 16;
+ CGFloat pageControlMaxX = self.sendButton.qmui_left;
+ CGFloat pageControlMinX = self.qmui_width - pageControlMaxX;
+ self.pageControl.frame = CGRectMake(pageControlMinX, self.qmui_height - self.safeAreaInsets.bottom - self.pageControlMarginBottom - pageControlHeight, pageControlMaxX - pageControlMinX, pageControlHeight);
}
-
- self.sendButton.qmui_right = self.qmui_width - self.qmui_safeAreaInsets.right - self.sendButtonMargins.right;
- self.sendButton.qmui_bottom = self.qmui_height - self.qmui_safeAreaInsets.bottom - self.sendButtonMargins.bottom;
-
- CGFloat pageControlHeight = 16;
- CGFloat pageControlMaxX = self.sendButton.qmui_left;
- CGFloat pageControlMinX = self.qmui_width - pageControlMaxX;
- self.pageControl.frame = CGRectMake(pageControlMinX, self.qmui_height - self.qmui_safeAreaInsets.bottom - self.pageControlMarginBottom - pageControlHeight, pageControlMaxX - pageControlMinX, pageControlHeight);
}
- (void)pageEmotions {
@@ -302,6 +493,16 @@ - (void)handlePageControlEvent:(UIPageControl *)pageControl {
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:pageControl.currentPage inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
+- (void)adjustEmotionsAlpha {
+ CGFloat x = MIN(self.deleteButton.frame.origin.x, self.sendButton.frame.origin.x);
+ CGFloat y = MIN(self.deleteButton.frame.origin.y, self.sendButton.frame.origin.y);
+ CGFloat width = CGRectGetMaxX(self.sendButton.frame) - CGRectGetMinX(self.deleteButton.frame);
+ CGFloat height = MAX(CGRectGetMaxY(self.deleteButton.frame), CGRectGetMaxY(self.sendButton.frame)) - MIN(CGRectGetMinY(self.deleteButton.frame), CGRectGetMinY(self.sendButton.frame));
+ CGRect buttonGruopRect = CGRectMake(x, y, width, height);
+ CGRect floatingRect = [self.verticalScrollView convertRect:buttonGruopRect fromView:self];
+ [self.verticalScrollView adjustEmotionsAlphaWithFloatingRect:floatingRect];
+}
+
#pragma mark - UIAppearance Setter
- (void)setSendButtonTitleAttributes:(NSDictionary *)sendButtonTitleAttributes {
@@ -319,6 +520,36 @@ - (void)setSendButtonCornerRadius:(CGFloat)sendButtonCornerRadius {
self.sendButton.layer.cornerRadius = _sendButtonCornerRadius;
}
+- (void)setDeleteButtonBackgroundColor:(UIColor *)deleteButtonBackgroundColor {
+ _deleteButtonBackgroundColor = deleteButtonBackgroundColor;
+ self.deleteButton.backgroundColor = deleteButtonBackgroundColor;
+}
+
+- (void)setDeleteButtonImage:(UIImage *)deleteButtonImage {
+ _deleteButtonImage = deleteButtonImage;
+ [self.deleteButton setImage:self.deleteButtonImage forState:UIControlStateNormal];
+}
+
+- (void)setDeleteButtonCornerRadius:(CGFloat)deleteButtonCornerRadius {
+ _deleteButtonCornerRadius = deleteButtonCornerRadius;
+ self.deleteButton.layer.cornerRadius = deleteButtonCornerRadius;
+}
+
+#pragma mark -
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
+ if (scrollView == self.verticalScrollView) {
+ [self adjustEmotionsAlpha];
+ } else if (scrollView == self.collectionView) {
+ CGFloat index = scrollView.contentOffset.x / scrollView.bounds.size.width;
+ if (ceil(index) == floor(index)) {
+ // 滚到到整页,需要调用 updateDeleteButton: 重新设置一次删除按钮,否则有可能是截图按钮
+ QMUIEmotionPageView *pageView = (QMUIEmotionPageView *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
+ [pageView updateDeleteButton:self.deleteButton];
+ }
+ }
+}
+
#pragma mark -
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
@@ -334,8 +565,8 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell
pageView.emotionSize = self.emotionSize;
pageView.emotionSelectedBackgroundExtension = self.emotionSelectedBackgroundExtension;
pageView.minimumEmotionHorizontalSpacing = self.minimumEmotionHorizontalSpacing;
- [pageView.deleteButton setImage:self.deleteButtonImage forState:UIControlStateNormal];
- [pageView.deleteButton setImage:[self.deleteButtonImage qmui_imageWithAlpha:ButtonHighlightedAlpha] forState:UIControlStateHighlighted];
+ [pageView updateDeleteButton:self.deleteButton];
+ pageView.deleteButtonOffset = self.deleteButtonOffset;
pageView.debug = self.debug;
[pageView setNeedsDisplay];
return pageView;
@@ -357,12 +588,18 @@ - (void)emotionPageView:(QMUIEmotionPageView *)emotionPageView didSelectEmotion:
}
}
-- (void)didSelectDeleteButtonInEmotionPageView:(QMUIEmotionPageView *)emotionPageView {
- if (self.didSelectDeleteButtonBlock) {
- self.didSelectDeleteButtonBlock();
+- (void)emotionPageViewDidLayoutEmotions:(QMUIEmotionPageView *)emotionPageView {
+ if (self.verticalAlignment) {
+ [self adjustEmotionsAlpha];
}
}
+#pragma mark - Getter
+
+- (UIScrollView *)scrollView {
+ return self.verticalScrollView;
+}
+
@end
@interface QMUIEmotionView (UIAppearance)
@@ -380,7 +617,7 @@ + (void)initialize {
+ (void)setDefaultAppearance {
QMUIEmotionView *appearance = [QMUIEmotionView appearance];
- appearance.backgroundColor = UIColorWhite;
+ appearance.backgroundColor = UIColorForBackground;// 如果先设置了 UIView.appearance.backgroundColor,再使用最传统的 method_exchangeImplementations 交换 UIView.setBackgroundColor 方法,则会 crash。QMUI 这里是在 +initialize 时设置的,业务如果要 hook -[UIView setBackgroundColor:] 则需要比 +initialize 更早才行
appearance.deleteButtonImage = [QMUIHelper imageWithName:@"QMUI_emotion_delete"];
appearance.paddingInPage = UIEdgeInsetsMake(18, 18, 65, 18);
appearance.numberOfRowsPerPage = 4;
@@ -392,6 +629,8 @@ + (void)setDefaultAppearance {
appearance.sendButtonCornerRadius = 4;
appearance.sendButtonMargins = UIEdgeInsetsMake(0, 0, 16, 16);
appearance.pageControlMarginBottom = 22;
+ appearance.deleteButtonCornerRadius = 4;
+ appearance.emotionVerticalSpacing = 10;
UIPageControl *pageControlAppearance = [UIPageControl appearanceWhenContainedInInstancesOfClasses:@[[QMUIEmotionView class]]];
pageControlAppearance.pageIndicatorTintColor = UIColorMake(210, 210, 210);
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h
index 4797cfd8..c4d74647 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIEmptyView.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m
index f637fa44..ad7fec75 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIEmptyView.m
@@ -19,6 +19,7 @@
#import "NSParagraphStyle+QMUI.h"
#import "UIView+QMUI.h"
#import "QMUIButton.h"
+#import "QMUIAppearance.h"
@interface QMUIEmptyView ()
@@ -45,24 +46,10 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {
- (void)didInitialize {
// 系统默认会在view即将被add到window上时才设置这些值,这个时机有点晚了,因为我们可能在add到window之前就进行sizeThatFits计算或对view进行截图等操作,因此这里提前到init时就去做
- QMUIEmptyView *appearance = [QMUIEmptyView appearance];
- _imageViewInsets = appearance.imageViewInsets;
- _loadingViewInsets = appearance.loadingViewInsets;
- _textLabelInsets = appearance.textLabelInsets;
- _detailTextLabelInsets = appearance.detailTextLabelInsets;
- _actionButtonInsets = appearance.actionButtonInsets;
- _verticalOffset = appearance.verticalOffset;
- _textLabelFont = appearance.textLabelFont;
- _detailTextLabelFont = appearance.detailTextLabelFont;
- _actionButtonFont = appearance.actionButtonFont;
- _textLabelTextColor = appearance.textLabelTextColor;
- _detailTextLabelTextColor = appearance.detailTextLabelTextColor;
- _actionButtonTitleColor = appearance.actionButtonTitleColor;
+ [self qmui_applyAppearance];
self.scrollView = [[UIScrollView alloc] init];
- if (@available(iOS 11, *)) {
- self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- }
+ self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
self.scrollView.showsVerticalScrollIndicator = NO;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.scrollsToTop = NO;
@@ -110,7 +97,7 @@ - (void)layoutSubviews {
self.contentView.frame = CGRectSetY(self.contentView.frame, 0);
}
- self.scrollView.contentSize = CGSizeMake(fmax(CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset), contentViewSize.width), fmax(CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.contentInset), CGRectGetMaxY(self.contentView.frame)));
+ self.scrollView.contentSize = CGSizeMake(MAX(CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset), contentViewSize.width), MAX(CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.contentInset), CGRectGetMaxY(self.contentView.frame)));
CGFloat originY = 0;
@@ -144,27 +131,25 @@ - (void)layoutSubviews {
- (CGSize)sizeThatContentViewFits {
CGFloat resultWidth = CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset);
-
- CGFloat imageViewHeight = [self.imageView sizeThatFits:CGSizeMake(resultWidth, CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.imageViewInsets);
- CGFloat loadingViewHeight = CGRectGetHeight(self.loadingView.bounds) + UIEdgeInsetsGetVerticalValue(self.loadingViewInsets);
- CGFloat textLabelHeight = [self.textLabel sizeThatFits:CGSizeMake(resultWidth, CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.textLabelInsets);
- CGFloat detailTextLabelHeight = [self.detailTextLabel sizeThatFits:CGSizeMake(resultWidth, CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.detailTextLabelInsets);
- CGFloat actionButtonHeight = [self.actionButton sizeThatFits:CGSizeMake(resultWidth, CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.actionButtonInsets);
-
CGFloat resultHeight = 0;
if (!self.imageView.hidden) {
+ CGFloat imageViewHeight = [self.imageView sizeThatFits:CGSizeMake(resultWidth - UIEdgeInsetsGetHorizontalValue(self.imageViewInsets), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.imageViewInsets);
resultHeight += imageViewHeight;
}
if (!self.loadingView.hidden) {
+ CGFloat loadingViewHeight = CGRectGetHeight(self.loadingView.bounds) + UIEdgeInsetsGetVerticalValue(self.loadingViewInsets);
resultHeight += loadingViewHeight;
}
if (!self.textLabel.hidden) {
+ CGFloat textLabelHeight = [self.textLabel sizeThatFits:CGSizeMake(resultWidth - UIEdgeInsetsGetHorizontalValue(self.textLabelInsets), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.textLabelInsets);
resultHeight += textLabelHeight;
}
if (!self.detailTextLabel.hidden) {
+ CGFloat detailTextLabelHeight = [self.detailTextLabel sizeThatFits:CGSizeMake(resultWidth - UIEdgeInsetsGetHorizontalValue(self.detailTextLabelInsets), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.detailTextLabelInsets);
resultHeight += detailTextLabelHeight;
}
if (!self.actionButton.hidden) {
+ CGFloat actionButtonHeight = [self.actionButton sizeThatFits:CGSizeMake(resultWidth - UIEdgeInsetsGetHorizontalValue(self.actionButtonInsets), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.actionButtonInsets);
resultHeight += actionButtonHeight;
}
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h
index 7b547ae5..a362bbb2 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIFloatLayoutView.h
@@ -43,7 +43,7 @@ extern const CGSize QMUIFloatLayoutViewAutomaticalMaximumItemSize;
/**
* item 之间的间距,默认为 UIEdgeInsetsZero。
*
- * @warning 上、下、左、右四个边缘的 item 布局时不会考虑 itemMargins.left/bottom/left/right。
+ * @warning 上、下、左、右四个边缘的 item 布局时不会考虑 itemMargins.top/bottom/left/right。
*/
@property(nonatomic, assign) UIEdgeInsets itemMargins;
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m
index 56677993..2bfa426a 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIFloatLayoutView.m
@@ -63,37 +63,33 @@ - (CGSize)layoutSubviewsWithSize:(CGSize)size shouldLayout:(BOOL)shouldLayout {
CGPoint itemViewOrigin = CGPointMake(ValueSwitchAlignLeftOrRight(self.padding.left, size.width - self.padding.right), self.padding.top);
CGFloat currentRowMaxY = itemViewOrigin.y;
CGSize maximumItemSize = CGSizeEqualToSize(self.maximumItemSize, QMUIFloatLayoutViewAutomaticalMaximumItemSize) ? CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.padding), size.height - UIEdgeInsetsGetVerticalValue(self.padding)) : self.maximumItemSize;
-
- for (NSInteger i = 0, l = visibleItemViews.count; i < l; i ++) {
+ NSInteger line = -1;
+ for (NSInteger i = 0, l = visibleItemViews.count; i < l; i++) {
UIView *itemView = visibleItemViews[i];
+ CGRect itemViewFrame;
CGSize itemViewSize = [itemView sizeThatFits:maximumItemSize];
- itemViewSize.width = fmax(self.minimumItemSize.width, itemViewSize.width);
- itemViewSize.height = fmax(self.minimumItemSize.height, itemViewSize.height);
- itemViewSize.width = fmin(maximumItemSize.width, itemViewSize.width);
- itemViewSize.height = fmin(maximumItemSize.height, itemViewSize.height);
+ itemViewSize.width = MIN(maximumItemSize.width, MAX(self.minimumItemSize.width, itemViewSize.width));
+ itemViewSize.height = MIN(maximumItemSize.height, MAX(self.minimumItemSize.height, itemViewSize.height));
BOOL shouldBreakline = i == 0 ? YES : ValueSwitchAlignLeftOrRight(itemViewOrigin.x + self.itemMargins.left + itemViewSize.width + self.padding.right > size.width,
itemViewOrigin.x - self.itemMargins.right - itemViewSize.width - self.padding.left < 0);
if (shouldBreakline) {
+ line++;
+ currentRowMaxY += line > 0 ? self.itemMargins.top : 0;
// 换行,每一行第一个 item 是不考虑 itemMargins 的
- if (shouldLayout) {
- itemView.frame = CGRectMake(ValueSwitchAlignLeftOrRight(self.padding.left, size.width - self.padding.right - itemViewSize.width), currentRowMaxY + self.itemMargins.top, itemViewSize.width, itemViewSize.height);
- }
-
- itemViewOrigin.x = ValueSwitchAlignLeftOrRight(self.padding.left + itemViewSize.width + self.itemMargins.right, size.width - self.padding.right - itemViewSize.width - self.itemMargins.left);
- itemViewOrigin.y = currentRowMaxY;
+ itemViewFrame = CGRectMake(ValueSwitchAlignLeftOrRight(self.padding.left, size.width - self.padding.right - itemViewSize.width), currentRowMaxY, itemViewSize.width, itemViewSize.height);
+ itemViewOrigin.y = CGRectGetMinY(itemViewFrame);
} else {
// 当前行放得下
- if (shouldLayout) {
- itemView.frame = CGRectMake(ValueSwitchAlignLeftOrRight(itemViewOrigin.x + self.itemMargins.left, itemViewOrigin.x - self.itemMargins.right - itemViewSize.width), itemViewOrigin.y + self.itemMargins.top, itemViewSize.width, itemViewSize.height);
- }
-
- itemViewOrigin.x = ValueSwitchAlignLeftOrRight(itemViewOrigin.x + UIEdgeInsetsGetHorizontalValue(self.itemMargins) + itemViewSize.width,
- itemViewOrigin.x - itemViewSize.width - UIEdgeInsetsGetHorizontalValue(self.itemMargins));
+ itemViewFrame = CGRectMake(ValueSwitchAlignLeftOrRight(itemViewOrigin.x + self.itemMargins.left, itemViewOrigin.x - self.itemMargins.right - itemViewSize.width), itemViewOrigin.y, itemViewSize.width, itemViewSize.height);
}
+ itemViewOrigin.x = ValueSwitchAlignLeftOrRight(CGRectGetMaxX(itemViewFrame) + self.itemMargins.right, CGRectGetMinX(itemViewFrame) - self.itemMargins.left);
+ currentRowMaxY = MAX(currentRowMaxY, CGRectGetMaxY(itemViewFrame) + self.itemMargins.bottom);
- currentRowMaxY = fmax(currentRowMaxY, itemViewOrigin.y + UIEdgeInsetsGetVerticalValue(self.itemMargins) + itemViewSize.height);
+ if (shouldLayout) {
+ itemView.frame = itemViewFrame;
+ }
}
// 最后一行不需要考虑 itemMarins.bottom,所以这里减掉
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h
index 3319ac65..80e35b7a 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIGridView.h
@@ -30,6 +30,9 @@
/// 指定每一行的高度,默认为 0
@property(nonatomic, assign) IBInspectable CGFloat rowHeight;
+/// 内部的 padding,默认为 UIEdgeInsetsZero
+@property(nonatomic, assign) UIEdgeInsets padding;
+
/// 指定 item 之间的分隔线宽度,默认为 0
@property(nonatomic, assign) IBInspectable CGFloat separatorWidth;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m
index 408b35b5..b8214088 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIGridView.m
@@ -70,7 +70,7 @@ - (void)setSeparatorColor:(UIColor *)separatorColor {
// 返回最接近平均列宽的值,保证其为整数,因此所有columnWidth加起来可能比总宽度要小
- (CGFloat)stretchColumnWidth {
- return floor((CGRectGetWidth(self.bounds) - self.separatorWidth * (self.columnCount - 1)) / self.columnCount);
+ return floor((CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.padding) - self.separatorWidth * (self.columnCount - 1)) / self.columnCount);
}
- (NSInteger)rowCount {
@@ -81,6 +81,7 @@ - (NSInteger)rowCount {
- (CGSize)sizeThatFits:(CGSize)size {
NSInteger rowCount = [self rowCount];
CGFloat totalHeight = rowCount * self.rowHeight + (rowCount - 1) * self.separatorWidth;
+ totalHeight += UIEdgeInsetsGetVerticalValue(self.padding);
size.height = totalHeight;
return size;
}
@@ -109,15 +110,15 @@ - (void)layoutSubviews {
BOOL isLastRow = row == rowCount - 1;
UIView *subview = self.subviews[index];
- CGRect subviewFrame = CGRectMake(columnWidth * column + self.separatorWidth * column, rowHeight * row + self.separatorWidth * row, columnWidth, rowHeight);
+ CGRect subviewFrame = CGRectMake(columnWidth * column + self.separatorWidth * column + self.padding.left, rowHeight * row + self.separatorWidth * row + self.padding.top, columnWidth, rowHeight);
if (isLastColumn) {
// 每行最后一个item要占满剩余空间,否则可能因为strecthColumnWidth不精确导致右边漏空白
- subviewFrame.size.width = size.width - columnWidth * (self.columnCount - 1) - self.separatorWidth * (self.columnCount - 1);
+ subviewFrame.size.width = size.width - UIEdgeInsetsGetHorizontalValue(self.padding) - columnWidth * (self.columnCount - 1) - self.separatorWidth * (self.columnCount - 1);
}
if (isLastRow) {
// 最后一行的item要占满剩余空间,避免一些计算偏差
- subviewFrame.size.height = size.height - rowHeight * (rowCount - 1) - self.separatorWidth * (rowCount - 1);
+ subviewFrame.size.height = size.height - UIEdgeInsetsGetVerticalValue(self.padding) - rowHeight * (rowCount - 1) - self.separatorWidth * (rowCount - 1);
}
subview.frame = subviewFrame;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h
index 583e95a0..85838dc0 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePreviewView.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m
index fb5ee203..9aa70d68 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePreviewView.m
@@ -83,17 +83,15 @@ - (void)didInitializedWithFrame:(CGRect)frame {
self.collectionViewLayout.allowsMultipleItemScroll = NO;
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMakeWithSize(frame.size) collectionViewLayout:self.collectionViewLayout];
- self.collectionView.delegate = self;
self.collectionView.dataSource = self;
+ self.collectionView.delegate = self;
self.collectionView.backgroundColor = UIColorClear;
self.collectionView.showsHorizontalScrollIndicator = NO;
self.collectionView.showsVerticalScrollIndicator = NO;
self.collectionView.scrollsToTop = NO;
self.collectionView.delaysContentTouches = NO;
self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
- if (@available(iOS 11, *)) {
- self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- }
+ self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
[self.collectionView registerClass:[QMUIImagePreviewCell class] forCellWithReuseIdentifier:kImageOrUnknownCellIdentifier];
[self.collectionView registerClass:[QMUIImagePreviewCell class] forCellWithReuseIdentifier:kVideoCellIdentifier];
[self.collectionView registerClass:[QMUIImagePreviewCell class] forCellWithReuseIdentifier:kLivePhotoCellIdentifier];
@@ -169,9 +167,7 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell
// 因为 cell 复用的问题,很可能此时会显示一张错误的图片,因此这里要清空所有图片的显示
zoomView.image = nil;
zoomView.videoPlayerItem = nil;
- if (@available(iOS 9.1, *)) {
- zoomView.livePhoto = nil;
- }
+ zoomView.livePhoto = nil;
if ([self.delegate respondsToSelector:@selector(imagePreviewView:renderZoomImageView:atIndex:)]) {
[self.delegate imagePreviewView:self renderZoomImageView:zoomView atIndex:indexPath.item];
}
@@ -221,8 +217,15 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 在滑动过临界点的那一次才去调用 delegate,避免过于频繁的调用
BOOL isFirstDidScroll = self.previousIndexWhenScrolling == 0;
- BOOL turnPageToRight = betweenOrEqual(self.previousIndexWhenScrolling, floor(index) + 0.5, index);
- BOOL turnPageToLeft = betweenOrEqual(index, floor(index) + 0.5, self.previousIndexWhenScrolling);
+
+ // fastToRight example : self.previousIndexWhenScrolling 1.49, index = 2.0
+ BOOL fastToRight = (floor(index) - floor(self.previousIndexWhenScrolling) >= 1.0) && (floor(index) - self.previousIndexWhenScrolling > 0.5);
+ BOOL turnPageToRight = fastToRight || betweenOrEqual(self.previousIndexWhenScrolling, floor(index) + 0.5, index);
+
+ // fastToLeft example : self.previousIndexWhenScrolling 2.51, index = 1.99
+ BOOL fastToLeft = (floor(self.previousIndexWhenScrolling) - floor(index) >= 1.0) && (self.previousIndexWhenScrolling - ceil(index) > 0.5);
+ BOOL turnPageToLeft = fastToLeft || betweenOrEqual(index, floor(index) + 0.5, self.previousIndexWhenScrolling);
+
if (!isFirstDidScroll && (turnPageToRight || turnPageToLeft)) {
index = round(index);
if (0 <= index && index < [self.collectionView numberOfItemsInSection:0]) {
@@ -246,7 +249,7 @@ - (NSInteger)indexForZoomImageView:(QMUIZoomImageView *)zoomImageView {
if ([zoomImageView.superview.superview isKindOfClass:[QMUIImagePreviewCell class]]) {
return [self.collectionView indexPathForCell:(QMUIImagePreviewCell *)zoomImageView.superview.superview].item;
} else {
- NSAssert(NO, @"尝试通过 %s 获取 QMUIZoomImageView 所在的 index,但找不到 QMUIZoomImageView 所在的 cell,index 获取失败。%@", __func__, zoomImageView);
+ QMUIAssert(NO, @"QMUIImagePreviewView (QMUIZoomImageView)", @"尝试通过 %s 获取 QMUIZoomImageView 所在的 index,但找不到 QMUIZoomImageView 所在的 cell,index 获取失败。%@", __func__, zoomImageView);
}
return NSNotFound;
}
@@ -260,7 +263,7 @@ - (void)checkIfDelegateMissing {
#ifdef DEBUG
[NSObject qmui_enumerateProtocolMethods:@protocol(QMUIZoomImageViewDelegate) usingBlock:^(SEL selector) {
if (![self respondsToSelector:selector]) {
- NSAssert(NO, @"%@ 需要响应 %@ 的方法 -%@", NSStringFromClass([self class]), NSStringFromProtocol(@protocol(QMUIZoomImageViewDelegate)), NSStringFromSelector(selector));
+ QMUIAssert(NO, @"QMUIImagePreviewView (QMUIZoomImageView)", @"%@ 需要响应 %@ 的方法 -%@", NSStringFromClass([self class]), NSStringFromProtocol(@protocol(QMUIZoomImageViewDelegate)), NSStringFromSelector(selector));
}
}];
#endif
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h
index 873832dd..be6ff859 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePreviewViewController.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m
index 3775a826..167572ab 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePreviewViewController.m
@@ -19,28 +19,25 @@
#import "UIInterface+QMUI.h"
#import "UIView+QMUI.h"
#import "UIViewController+QMUI.h"
+#import "QMUIAppearance.h"
const CGFloat QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension = -1;
@implementation QMUIImagePreviewViewController (UIAppearance)
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
+}
+
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- [self appearance];
+ [self initAppearance];
});
}
-static QMUIImagePreviewViewController *imagePreviewViewControllerAppearance;
-+ (nonnull instancetype)appearance {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- if (!imagePreviewViewControllerAppearance) {
- imagePreviewViewControllerAppearance = [[QMUIImagePreviewViewController alloc] init];
- imagePreviewViewControllerAppearance.backgroundColor = UIColorBlack;
- }
- });
- return imagePreviewViewControllerAppearance;
++ (void)initAppearance {
+ QMUIImagePreviewViewController.appearance.backgroundColor = UIColorBlack;
}
@end
@@ -57,19 +54,19 @@ @interface QMUIImagePreviewViewController ()
@implementation QMUIImagePreviewViewController
-@synthesize imagePreviewView = _imagePreviewView;
-
- (void)didInitialize {
[super didInitialize];
- self.automaticallyAdjustsScrollViewInsets = NO;
self.sourceImageCornerRadius = QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension;
_dismissingGestureEnabled = YES;
- if (imagePreviewViewControllerAppearance) {
- self.backgroundColor = [QMUIImagePreviewViewController appearance].backgroundColor;
- }
+ [self qmui_applyAppearance];
+
+ self.qmui_prefersHomeIndicatorAutoHiddenBlock = ^BOOL{
+ return YES;
+ };
+
// present style
self.transitioningAnimator = [[QMUIImagePreviewViewTransitionAnimator alloc] init];
@@ -78,11 +75,6 @@ - (void)didInitialize {
self.transitioningDelegate = self;
}
-- (QMUIImagePreviewView *)imagePreviewView {
- [self loadViewIfNeeded];
- return _imagePreviewView;
-}
-
- (void)setBackgroundColor:(UIColor *)backgroundColor {
_backgroundColor = backgroundColor;
if ([self isViewLoaded]) {
@@ -90,14 +82,17 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor {
}
}
-- (void)viewDidLoad {
- [super viewDidLoad];
- self.view.backgroundColor = self.backgroundColor;
+@synthesize imagePreviewView = _imagePreviewView;
+- (QMUIImagePreviewView *)imagePreviewView {
+ if (!_imagePreviewView) {
+ _imagePreviewView = [[QMUIImagePreviewView alloc] initWithFrame:self.isViewLoaded ? self.view.bounds : CGRectZero];
+ }
+ return _imagePreviewView;
}
- (void)initSubviews {
[super initSubviews];
- _imagePreviewView = [[QMUIImagePreviewView alloc] initWithFrame:self.view.bounds];
+ self.view.backgroundColor = self.backgroundColor;
[self.view addSubview:self.imagePreviewView];
}
@@ -152,7 +147,8 @@ - (BOOL)prefersStatusBarHidden {
if (self.qmui_visibleState < QMUIViewControllerDidAppear || self.qmui_visibleState >= QMUIViewControllerDidDisappear) {
// 在 present/dismiss 动画过程中,都使用原界面的状态栏显隐状态
if (self.presentingViewController) {
- self.originalStatusBarHidden = self.presentingViewController.qmui_prefersStatusBarHidden;
+ BOOL statusBarHidden = self.presentingViewController.view.window.windowScene.statusBarManager.statusBarHidden;
+ self.originalStatusBarHidden = statusBarHidden;
return self.originalStatusBarHidden;
}
return [super prefersStatusBarHidden];
@@ -160,10 +156,6 @@ - (BOOL)prefersStatusBarHidden {
return self.statusBarHidden;
}
-- (BOOL)prefersHomeIndicatorAutoHidden {
- return YES;
-}
-
#pragma mark - 动画
- (void)initObjectsForZoomStyleIfNeeded {
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h
index fa32be33..543aa393 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h
@@ -1,17 +1,16 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePreviewViewTransitionAnimator.h
// QMUIKit
//
// Created by MoLice on 2018/D/19.
-// Copyright © 2018 QMUI Team. All rights reserved.
//
#import
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m
index 6ade5cc3..f8c12b26 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m
@@ -1,17 +1,16 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIImagePreviewViewTransitionAnimator.m
// QMUIKit
//
// Created by MoLice on 2018/D/19.
-// Copyright © 2018 QMUI Team. All rights reserved.
//
#import "QMUIImagePreviewViewTransitionAnimator.h"
@@ -178,7 +177,11 @@ - (void)animateTransition:(nonnull id)tran
UIView *containerView = transitionContext.containerView;
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
+ [fromView setNeedsLayout];
+ [fromView layoutIfNeeded];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
+ [toView setNeedsLayout];
+ [toView layoutIfNeeded];// present 时 toViewController 还没走到 viewDidLayoutSubviews,此时做动画可能得到不正确的布局,所以强制布局一次
QMUIZoomImageView *zoomImageView = [self.imagePreviewViewController.imagePreviewView zoomImageViewAtIndex:self.imagePreviewViewController.imagePreviewView.currentImageIndex];
toView.frame = containerView.bounds;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h
index ac546230..b7da3997 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIKeyboardManager.h
@@ -19,12 +19,11 @@
@protocol QMUIKeyboardManagerDelegate;
@class QMUIKeyboardUserInfo;
-/// 注意:由于某些Bug(例如 iOS 8 的 iPad 修改切换键盘类型,delegate 回调键盘高度值错误),QMUIKeyboardManager 不再支持 iPad 的浮动键盘了 - 更新于 2017.12.8 ///
-/// 注意:QMUI 已经废弃 iOS8 了,所以浮动键盘又可以支持了
-
/**
- * `QMUIKeyboardManager` 提供了方便管理键盘事件的方案,使用的场景是需要跟随键盘的显示或者隐藏来更改界面的 UI,例如输入框跟随在键盘的顶部。
+ * `QMUIKeyboardManager` 提供了方便、稳定的管理键盘事件的方案,使用的场景是需要跟随键盘的显示或者隐藏来更改界面的 UI,例如输入框跟随在键盘的顶部。
* 由于键盘通知是整个 App 全局的,所以经常会遇到 A 的键盘监听回调里接收到 B 的键盘事件,这样的情况往往不是我们想要的,即使可以通过判断当前的 firstResponder 来区分,但还是不能完美的解决问题或者有时候解决起来非常麻烦。`QMUIKeyboardManager` 通过 `delegateEnabled` 和 `targetResponder` 等属性来方便地控制 firstResponder,从而可以实现某个键盘监听回调方法只响应某个 UIResponder 或者某几个 UIResponder 触发的键盘通知。
+ * 另外系统的“设置→辅助功能→动态效果→减弱动态效果→首选交叉淡出过渡效果”会改变系统键盘的动画,QMUIKeyboardManager 也兼容了这种情况,如果业务自己处理,很容易会遗漏。
+ *
* 使用方式:
* 1. 使用 initWithDelegate: 方法初始化
* 2. 通过 addTargetResponder: 的方式将要监听的输入框添加进来
@@ -52,6 +51,14 @@
*/
@property(nonatomic, assign) BOOL delegateEnabled;
+/**
+ * 是否忽视 `applicationState` 状态的影响。默认为 NO,也即只有 `UIApplicationStateActive` 才会响应通知,如果设置为 YES,则任何 state 都会响应通知。
+ */
+@property(nonatomic, assign) BOOL ignoreApplicationState UI_APPEARANCE_SELECTOR;
+
+
++ (instancetype)appearance;
+
/**
* 添加触发键盘事件的 UIResponder,一般是 UITextView 或者 UITextField ,不添加 targetResponder 的话,则默认接受任何 UIResponder 产生的键盘通知。
* 添加成功将会返回YES,否则返回NO。
@@ -182,6 +189,11 @@
*/
@property(nonatomic, assign, readonly) UIViewAnimationOptions animationOptions;
+/**
+ * 当前是否浮动键盘
+ */
+@property(nonatomic, assign, readonly) BOOL isFloatingKeyboard;
+
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m
index cc04c0a5..178df3ec 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIKeyboardManager.m
@@ -16,11 +16,18 @@
#import "QMUIKeyboardManager.h"
#import "QMUICore.h"
#import "QMUILog.h"
+#import "QMUIAppearance.h"
+#import "QMUIMultipleDelegates.h"
+#import "NSArray+QMUI.h"
+#import "UIView+QMUI.h"
+
+@class QMUIKeyboardViewFrameObserver;
+@protocol QMUIKeyboardViewFrameObserverDelegate
+@required
+- (void)keyboardViewFrameDidChange:(UIView *)keyboardView;
+@end
-
-static QMUIKeyboardManager *kKeyboardManagerInstance;
-
-@interface QMUIKeyboardManager ()
+@interface QMUIKeyboardManager ()
@property(nonatomic, strong) NSMutableArray *targetResponderValues;
@@ -28,7 +35,7 @@ @interface QMUIKeyboardManager ()
@property(nonatomic, assign) CGRect keyboardMoveBeginRect;
@property(nonatomic, weak) UIResponder *currentResponder;
-@property(nonatomic, weak) UIResponder *currentResponderWhenResign;
+//@property(nonatomic, weak) UIResponder *currentResponderWhenResign;
@property(nonatomic, assign) BOOL debug;
@@ -74,10 +81,7 @@ + (void)load {
dispatch_once(&onceToken, ^{
OverrideImplementation([UIResponder class], @selector(becomeFirstResponder), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^BOOL(UIResponder *selfObject) {
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- selfObject.keyboardManager_isFirstResponder = YES;
- }
+ selfObject.keyboardManager_isFirstResponder = YES;
// call super
BOOL (*originSelectorIMP)(id, SEL);
@@ -90,16 +94,12 @@ + (void)load {
OverrideImplementation([UIResponder class], @selector(resignFirstResponder), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^BOOL(UIResponder *selfObject) {
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- selfObject.keyboardManager_isFirstResponder = NO;
- if (selfObject.isFirstResponder &&
- selfObject.qmui_keyboardManager &&
- [selfObject.qmui_keyboardManager.allTargetResponders containsObject:selfObject]) {
- selfObject.qmui_keyboardManager.currentResponderWhenResign = selfObject;
- }
- }
-
+ selfObject.keyboardManager_isFirstResponder = NO;
+// if (selfObject.isFirstResponder &&
+// selfObject.qmui_keyboardManager &&
+// [selfObject.qmui_keyboardManager.allTargetResponders containsObject:selfObject]) {
+// selfObject.qmui_keyboardManager.currentResponderWhenResign = selfObject;
+// }
// call super
BOOL (*originSelectorIMP)(id, SEL);
originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider();
@@ -116,7 +116,7 @@ + (void)load {
@interface QMUIKeyboardViewFrameObserver : NSObject
-@property (nonatomic, copy) void (^keyboardViewChangeFrameBlock)(UIView *keyboardView);
+@property (nonatomic, weak) id delegate;
- (void)addToKeyboardView:(UIView *)keyboardView;
+ (instancetype)observerForView:(UIView *)keyboardView;
@@ -187,8 +187,8 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
}
id newValue = [change objectForKey:NSKeyValueChangeNewKey];
if (newValue == [NSNull null]) { newValue = nil; }
- if (self.keyboardViewChangeFrameBlock) {
- self.keyboardViewChangeFrameBlock(_keyboardView);
+ if (self.delegate) {
+ [self.delegate keyboardViewFrameDidChange:_keyboardView];
}
}
@@ -212,6 +212,8 @@ @interface QMUIKeyboardUserInfo ()
@property(nonatomic, assign, readwrite) UIViewAnimationCurve animationCurve;
@property(nonatomic, assign, readwrite) UIViewAnimationOptions animationOptions;
+@property(nonatomic, assign, readwrite) BOOL isFloatingKeyboard;
+
@end
@implementation QMUIKeyboardUserInfo
@@ -219,11 +221,26 @@ @implementation QMUIKeyboardUserInfo
- (void)setNotification:(NSNotification *)notification {
_notification = notification;
if (self.originUserInfo) {
+
_animationDuration = [[self.originUserInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
+
_animationCurve = (UIViewAnimationCurve)[[self.originUserInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
+
_animationOptions = self.animationCurve<<16;
- _beginFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
- _endFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
+
+ CGRect beginFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
+ CGRect endFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
+
+ // iOS 13 分屏键盘 x 不是 0,不知道是系统 BUG 还是故意这样,先这样保护,再观察一下后面的 beta 版本
+ if (IS_SPLIT_SCREEN_IPAD && beginFrame.origin.x > 0) {
+ beginFrame.origin.x = 0;
+ }
+ if (IS_SPLIT_SCREEN_IPAD && endFrame.origin.x > 0) {
+ endFrame.origin.x = 0;
+ }
+
+ _beginFrame = beginFrame;
+ _endFrame = endFrame;
}
}
@@ -300,13 +317,8 @@ - (UIViewAnimationOptions)animationOptions {
*/
@implementation QMUIKeyboardManager
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- if (!kKeyboardManagerInstance) {
- kKeyboardManagerInstance = [[QMUIKeyboardManager alloc] initWithDelegate:nil];
- }
- });
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
}
- (instancetype)init {
@@ -325,6 +337,7 @@ - (instancetype)initWithDelegate:(id )delegate {
_delegateEnabled = YES;
_targetResponderValues = [[NSMutableArray alloc] init];
[self addKeyboardNotification];
+ [self qmui_applyAppearance];
}
return self;
}
@@ -383,10 +396,10 @@ - (UIResponder *)unPackageTargetResponder:(NSValue *)value {
}
- (UIResponder *)firstResponderInWindows {
- UIResponder *responder = [[UIApplication sharedApplication].keyWindow qmui_findFirstResponder];
+ UIResponder *responder = [UIApplication.sharedApplication.keyWindow qmui_findFirstResponder];
if (!responder) {
- for (UIWindow *window in [UIApplication sharedApplication].windows) {
- if (window != [UIApplication sharedApplication].keyWindow) {
+ for (UIWindow *window in UIApplication.sharedApplication.windows) {
+ if (window != UIApplication.sharedApplication.keyWindow) {
responder = [window qmui_findFirstResponder];
if (responder) {
return responder;
@@ -408,14 +421,24 @@ - (void)addKeyboardNotification {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidChangeFrameNotification:) name:UIKeyboardDidChangeFrameNotification object:nil];
}
-- (BOOL)isAppActive:(NSNotification *)notification {
- if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) {
- return NO;
+- (BOOL)isAppActive {
+ if (self.ignoreApplicationState) {
+ return YES;
}
- if (![[notification.userInfo valueForKey:UIKeyboardIsLocalUserInfoKey] boolValue]) {
- return NO;
+ if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) {
+ return YES;
}
- return YES;
+ return NO;
+}
+
+- (BOOL)isLocalKeyboard:(NSNotification *)notification {
+ if ([[notification.userInfo valueForKey:UIKeyboardIsLocalUserInfoKey] boolValue]) {
+ return YES;
+ }
+ if (IS_SPLIT_SCREEN_IPAD) {
+ return YES;
+ }
+ return NO;
}
- (void)keyboardWillShowNotification:(NSNotification *)notification {
@@ -424,7 +447,7 @@ - (void)keyboardWillShowNotification:(NSNotification *)notification {
QMUILog(NSStringFromClass(self.class), @"keyboardWillShowNotification - %@", self);
}
- if (![self isAppActive:notification]) {
+ if (![self isAppActive] || ![self isLocalKeyboard:notification]) {
QMUILog(NSStringFromClass(self.class), @"app is not active");
return;
}
@@ -453,7 +476,7 @@ - (void)keyboardDidShowNotification:(NSNotification *)notification {
QMUILog(NSStringFromClass(self.class), @"keyboardDidShowNotification - %@", self);
}
- if (![self isAppActive:notification]) {
+ if (![self isAppActive] || ![self isLocalKeyboard:notification]) {
QMUILog(NSStringFromClass(self.class), @"app is not active");
return;
}
@@ -482,7 +505,7 @@ - (void)keyboardWillHideNotification:(NSNotification *)notification {
QMUILog(NSStringFromClass(self.class), @"keyboardWillHideNotification - %@", self);
}
- if (![self isAppActive:notification]) {
+ if (![self isAppActive] || ![self isLocalKeyboard:notification]) {
QMUILog(NSStringFromClass(self.class), @"app is not active");
return;
}
@@ -511,7 +534,7 @@ - (void)keyboardDidHideNotification:(NSNotification *)notification {
QMUILog(NSStringFromClass(self.class), @"keyboardDidHideNotification - %@", self);
}
- if (![self isAppActive:notification]) {
+ if (![self isAppActive] || ![self isLocalKeyboard:notification]) {
QMUILog(NSStringFromClass(self.class), @"app is not active");
return;
}
@@ -527,6 +550,7 @@ - (void)keyboardDidHideNotification:(NSNotification *)notification {
}
if (self.currentResponder && !self.currentResponder.keyboardManager_isFirstResponder && !IS_IPAD) {
+ // 时机最晚,设置为 nil
self.currentResponder = nil;
}
@@ -544,7 +568,7 @@ - (void)keyboardWillChangeFrameNotification:(NSNotification *)notification {
QMUILog(NSStringFromClass(self.class), @"keyboardWillChangeFrameNotification - %@", self);
}
- if (![self isAppActive:notification]) {
+ if (![self isAppActive] || ![self isLocalKeyboard:notification]) {
QMUILog(NSStringFromClass(self.class), @"app is not active");
return;
}
@@ -552,9 +576,7 @@ - (void)keyboardWillChangeFrameNotification:(NSNotification *)notification {
QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification];
self.lastUserInfo = userInfo;
- if ([self shouldReceiveShowNotification]) {
- userInfo.targetResponder = self.currentResponder ?: nil;
- } else if ([self shouldReceiveHideNotification]) {
+ if ([self shouldReceiveShowNotification] || [self shouldReceiveHideNotification]) {
userInfo.targetResponder = self.currentResponder ?: nil;
} else {
return;
@@ -576,7 +598,7 @@ - (void)keyboardDidChangeFrameNotification:(NSNotification *)notification {
QMUILog(NSStringFromClass(self.class), @"keyboardDidChangeFrameNotification - %@", self);
}
- if (![self isAppActive:notification]) {
+ if (![self isAppActive] || ![self isLocalKeyboard:notification]) {
QMUILog(NSStringFromClass(self.class), @"app is not active");
return;
}
@@ -584,9 +606,7 @@ - (void)keyboardDidChangeFrameNotification:(NSNotification *)notification {
QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification];
self.lastUserInfo = userInfo;
- if ([self shouldReceiveShowNotification]) {
- userInfo.targetResponder = self.currentResponder ?: nil;
- } else if ([self shouldReceiveHideNotification]) {
+ if ([self shouldReceiveShowNotification] || [self shouldReceiveHideNotification]) {
userInfo.targetResponder = self.currentResponder ?: nil;
} else {
return;
@@ -610,9 +630,13 @@ - (QMUIKeyboardUserInfo *)newUserInfoWithNotification:(NSNotification *)notifica
}
- (BOOL)shouldReceiveShowNotification {
- // 这里有BUG,如果点击了webview导致键盘下降,这个时候运行shouldReceiveHideNotification就会判断错误
- self.currentResponder = self.currentResponderWhenResign ?: [self firstResponderInWindows];
- self.currentResponderWhenResign = nil;
+ UIResponder *firstResponder = [self firstResponderInWindows];
+ // 如果点击了 webview 导致键盘下降,这个时候运行 shouldReceiveHideNotification 就会判断错误,所以如果发现是 nil 或是 WKContentView 则值不变
+ // WKContentView
+ if (!self.currentResponder || (firstResponder && ![firstResponder isKindOfClass:NSClassFromString([NSString stringWithFormat:@"%@%@", @"WK", @"ContentView"])])) {
+ self.currentResponder = firstResponder;
+ }
+
if (self.targetResponderValues.count <= 0) {
return YES;
} else {
@@ -638,16 +662,15 @@ - (void)addFrameObserverIfNeeded {
if (![self.class keyboardView]) {
return;
}
- __weak __typeof(self)weakSelf = self;
- QMUIKeyboardViewFrameObserver *observer = [QMUIKeyboardViewFrameObserver observerForView:[self.class keyboardView]];
+ UIView *keyboardView = [self.class keyboardView];
+ QMUIKeyboardViewFrameObserver *observer = [QMUIKeyboardViewFrameObserver observerForView:keyboardView];
if (!observer) {
observer = [[QMUIKeyboardViewFrameObserver alloc] init];
- observer.keyboardViewChangeFrameBlock = ^(UIView *keyboardView) {
- [weakSelf keyboardDidChangedFrame:keyboardView];
- };
- [observer addToKeyboardView:[self.class keyboardView]];
- [self keyboardDidChangedFrame:[self.class keyboardView]]; // 手动调用第一次
+ observer.qmui_multipleDelegatesEnabled = YES;
+ [observer addToKeyboardView:keyboardView];
}
+ observer.delegate = self;
+ [self keyboardDidChangedFrame:keyboardView]; // 手动调用第一次
}
- (void)keyboardDidChangedFrame:(UIView *)keyboardView {
@@ -686,10 +709,10 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView {
keyboardMoveUserInfo.animationOptions = self.lastUserInfo ? self.lastUserInfo.animationOptions : keyboardMoveUserInfo.animationCurve<<16;
keyboardMoveUserInfo.beginFrame = self.keyboardMoveBeginRect;
keyboardMoveUserInfo.endFrame = endFrame;
+ keyboardMoveUserInfo.isFloatingKeyboard = keyboardView ? CGRectGetWidth(keyboardView.bounds) < CGRectGetWidth(UIApplication.sharedApplication.delegate.window.bounds) : NO;
if (self.debug) {
- NSLog(@"keyboardDidMoveNotification - %@", self);
- NSLog(@"\n");
+ NSLog(@"keyboardDidMoveNotification - %@\n", self);
}
[self.delegate keyboardWillChangeFrameWithUserInfo:keyboardMoveUserInfo];
@@ -697,7 +720,7 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView {
self.keyboardMoveBeginRect = endFrame;
if (self.currentResponder) {
- UIWindow *mainWindow = [UIApplication sharedApplication].keyWindow ?: [[UIApplication sharedApplication] windows].firstObject;
+ UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window;
if (mainWindow) {
CGRect keyboardRect = keyboardMoveUserInfo.endFrame;
CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:mainWindow keyboardRect:keyboardRect];
@@ -718,6 +741,12 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView {
}
}
+#pragma mark -
+
+- (void)keyboardViewFrameDidChange:(UIView *)keyboardView {
+ [self keyboardDidChangedFrame:keyboardView];
+}
+
#pragma mark - 工具方法
+ (void)animateWithAnimated:(BOOL)animated keyboardUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion {
@@ -744,9 +773,12 @@ + (void)animateWithAnimated:(BOOL)animated keyboardUserInfo:(QMUIKeyboardUserInf
+ (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo showBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))showBlock hideBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))hideBlock {
// 专门处理 iPad Pro 在键盘完全不显示的情况(不会调用willShow,所以通过是否focus来判断)
// iPhoneX Max 这里键盘高度不是0,而是一个很小的值
- if ([QMUIKeyboardManager visibleKeyboardHeight] <= 0 && !keyboardUserInfo.isTargetResponderFocused) {
- if (hideBlock) {
- hideBlock(keyboardUserInfo);
+ if (!keyboardUserInfo.isTargetResponderFocused) {
+ // 先判断 focus,避免 frame 变化但是此时 visibleKeyboardHeight 还不是 0 导致调用了 showBlock
+ if ([QMUIKeyboardManager visibleKeyboardHeight] <= 0) {
+ if (hideBlock) {
+ hideBlock(keyboardUserInfo);
+ }
}
} else {
if (showBlock) {
@@ -755,39 +787,13 @@ + (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardU
}
}
-+ (UIWindow *)keyboardWindow {
-
- for (UIWindow *window in [UIApplication sharedApplication].windows) {
- if ([self getKeyboardViewFromWindow:window]) {
- return window;
- }
- }
-
- NSMutableArray *kbWindows = nil;
-
- for (UIWindow *window in [UIApplication sharedApplication].windows) {
- NSString *windowName = NSStringFromClass(window.class);
- if (windowName.length == 22 && [windowName hasPrefix:@"UI"] && [windowName hasSuffix:[NSString stringWithFormat:@"%@%@", @"Remote", @"KeyboardWindow"]]) {
- // UIRemoteKeyboardWindow(iOS9 以下 UITextEffectsWindow)
- if (!kbWindows) kbWindows = [NSMutableArray new];
- [kbWindows addObject:window];
- }
- }
-
- if (kbWindows.count == 1) {
- return kbWindows.firstObject;
- }
-
- return nil;
-}
-
+ (CGRect)convertKeyboardRect:(CGRect)rect toView:(UIView *)view {
if (CGRectIsNull(rect) || CGRectIsInfinite(rect)) {
return rect;
}
- UIWindow *mainWindow = [UIApplication sharedApplication].keyWindow ?: [UIApplication sharedApplication].windows.firstObject;
+ UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window;
if (!mainWindow) {
if (view) {
[view convertRect:rect fromView:nil];
@@ -825,40 +831,71 @@ + (CGFloat)distanceFromMinYToBottomInView:(UIView *)view keyboardRect:(CGRect)re
return distance;
}
+/**
+ 从所有 window 里寻找代表键盘当前布局位置的 view。
+ iOS 15 及以前(包括用 Xcode 13 编译的 App 运行在 iOS 16 上的场景),键盘的 UI 层级是:
+ |- UIApplication.windows
+ |- UIRemoteKeyboardWindow
+ |- UIInputSetContainerView
+ |- UIInputSetHostView - 键盘及 webView 里的输入工具栏(上下键、Done键)
+ |- _UIKBCompatInputView - 键盘主体按键
+ |- TUISystemInputAssistantView - 键盘顶部的候选词栏、emoji 键盘顶部的搜索框
+ |- _UIRemoteKeyboardPlaceholderView - webView 里的输入工具栏的占位(实际的 view 在 UITextEffectsWindow 里)
+
+ iOS 16 及以后(仅限用 Xcode 14 及以上版本编译的 App),UIApplication.windows 里已经不存在 UIRemoteKeyboardWindow 了,所以退而求其次,我们通过 UITextEffectsWindow 里的 UIInputSetHostView 来获取键盘的位置——这两个 window 在布局层面可以理解为镜像关系。
+ |- UIApplication.windows
+ |- UITextEffectsWindow
+ |- UIInputSetContainerView
+ |- UIInputSetHostView - 键盘及 webView 里的输入工具栏(上下键、Done键)
+ |- _UIRemoteKeyboardPlaceholderView - 整个键盘区域,包含顶部候选词栏、emoji 键盘顶部搜索栏(有时候不一定存在)
+ |- UIWebFormAccessory - webView 里的输入工具栏的占位
+ |- TUIInputAssistantHostView - 外接键盘时可能存在,此时不一定有 placeholder
+ |- UIInputSetHostView - 可能存在多个,但只有一个里面有 _UIRemoteKeyboardPlaceholderView
+
+ 所以只要找到 UIInputSetHostView 即可,优先从 UIRemoteKeyboardWindow 找,不存在的话则从 UITextEffectsWindow 找。
+ */
+ (UIView *)keyboardView {
- for (UIWindow *window in [UIApplication sharedApplication].windows) {
- UIView *view = [self getKeyboardViewFromWindow:window];
- if (view) {
- return view;
- }
- }
- return nil;
-}
-
-+ (UIView *)getKeyboardViewFromWindow:(UIWindow *)window {
+ UIView *inputSetHostView = [[UIApplication.sharedApplication.windows qmui_filterWithBlock:^BOOL(__kindof UIWindow * _Nonnull window) {
+ return [NSStringFromClass(window.class) isEqualToString:@"UIRemoteKeyboardWindow"];
+ }] qmui_compactMapWithBlock:^id _Nullable(__kindof UIWindow * _Nonnull window) {
+ return [self inputSetHostViewInWindow:window];
+ }].firstObject;
- if (!window) return nil;
+ if (inputSetHostView) return inputSetHostView;
- NSString *windowName = NSStringFromClass(window.class);
- if (![windowName isEqualToString:@"UIRemoteKeyboardWindow"]) {
- return nil;
- }
+ inputSetHostView = [[UIApplication.sharedApplication.windows qmui_filterWithBlock:^BOOL(__kindof UIWindow * _Nonnull window) {
+ return [NSStringFromClass(window.class) isEqualToString:@"UITextEffectsWindow"];
+ }] qmui_compactMapWithBlock:^id _Nullable(__kindof UIWindow * _Nonnull window) {
+ return [self inputSetHostViewInWindow:window];
+ }].firstObject;
- for (UIView *view in window.subviews) {
- NSString *viewName = NSStringFromClass(view.class);
- if (![viewName isEqualToString:@"UIInputSetContainerView"]) {
- continue;
- }
- for (UIView *subView in view.subviews) {
- NSString *subViewName = NSStringFromClass(subView.class);
- if (![subViewName isEqualToString:@"UIInputSetHostView"]) {
- continue;
- }
- return subView;
- }
+ return inputSetHostView;
+}
+
++ (UIView *)inputSetHostViewInWindow:(UIWindow *)window {
+ UIView *result = [[window.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) {
+ return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetContainerView"];
+ }].subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) {
+ return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetHostView"] && subview.subviews.count;
+ }];
+ return result;
+}
+
++ (UIWindow *)keyboardWindow {
+ UIView *inputSetHostView = [self keyboardView];
+ if (inputSetHostView) return inputSetHostView.window;
+
+ UIWindow *window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) {
+ return [NSStringFromClass(item.class) isEqualToString:@"UIRemoteKeyboardWindow"];
+ }];
+ if (window) {
+ return window;
}
- return nil;
+ window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) {
+ return [NSStringFromClass(item.class) isEqualToString:@"UITextEffectsWindow"];
+ }];
+ return window;
}
+ (BOOL)isKeyboardVisible {
@@ -889,11 +926,21 @@ + (CGRect)currentKeyboardFrame {
+ (CGFloat)visibleKeyboardHeight {
UIView *keyboardView = [self keyboardView];
- UIWindow *keyboardWindow = keyboardView.window;
+ // iPad“侧拉”模式打开的 App,App Window 和键盘 Window 尺寸不同,如果以键盘 Window 为准则会认为键盘一直在屏幕上,从而出现误判,所以这里改为用 App Window。
+ // iPhone、iPad 全屏/分屏/台前调度,都没这个问题
+// UIWindow *keyboardWindow = keyboardView.window;
+ UIWindow *keyboardWindow = UIApplication.sharedApplication.delegate.window;
if (!keyboardView || !keyboardWindow) {
return 0;
} else {
- CGRect visibleRect = CGRectIntersection(CGRectFlatted(keyboardWindow.bounds), CGRectFlatted(keyboardView.frame));
+ // 开启了系统的“设置→辅助功能→动态效果→减弱动态效果→首选交叉淡出过渡效果”后,键盘动画不再是 slide,而是 fade,此时应该用 alpha 来判断
+ // https://github.com/Tencent/QMUI_iOS/issues/1173
+ if (keyboardView.alpha <= 0) {
+ return 0;
+ }
+
+ CGRect keyboardFrame = [keyboardWindow qmui_convertRect:keyboardView.bounds fromView:keyboardView];
+ CGRect visibleRect = CGRectIntersection(keyboardWindow.bounds, keyboardFrame);
if (CGRectIsValidated(visibleRect)) {
return CGRectGetHeight(visibleRect);
}
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILabel.h b/QMUI/QMUIKit/QMUIComponents/QMUILabel.h
index 4c0108c0..4e5f0bac 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILabel.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILabel.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILabel.h
@@ -15,6 +15,8 @@
#import
+NS_ASSUME_NONNULL_BEGIN
+
/**
* `QMUILabel`支持通过`contentEdgeInsets`属性来实现类似padding的效果。
*
@@ -25,17 +27,28 @@
/// 控制label内容的padding,默认为UIEdgeInsetsZero
@property(nonatomic,assign) UIEdgeInsets contentEdgeInsets;
+/// 支持在 label 无法显示完整文字时在 label 的末尾显示一个自定义的 View(通常用来实现点击展开更多的交互)
+@property(nonatomic, strong, nullable) __kindof UIView *truncatingTailView;
+
/// 是否需要长按复制的功能,默认为 NO。
/// 长按时的背景色通过`highlightedBackgroundColor`设置。
@property(nonatomic,assign) IBInspectable BOOL canPerformCopyAction;
/// 当 canPerformCopyAction 开启时,长按出来的菜单上的复制按钮的文本,默认为 nil,nil 时 menuItem 上的文字为“复制”
-@property(nonatomic, copy) IBInspectable NSString *menuItemTitleForCopyAction;
+@property(nonatomic, copy, nullable) IBInspectable NSString *menuItemTitleForCopyAction;
-/// 如果打开了`canPerformCopyAction`,则长按时背景色将会被改为`highlightedBackgroundColor`
-@property(nonatomic,strong) IBInspectable UIColor *highlightedBackgroundColor;
+/**
+ label 在 highlighted 时的背景色,通常用于两种场景:
+ 1. 开启了 canPerformCopyAction 时,长按后的背景色
+ 2. 作为 subviews 放在 UITableViewCell 上,当 cell highlighted 时,label 也会触发 highlighted,此时背景色也会显示为这个属性的值
+
+ 默认为 nil
+*/
+@property(nonatomic,strong, nullable) IBInspectable UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR;
/// 点击了“复制”后的回调
-@property(nonatomic, copy) void (^didCopyBlock)(QMUILabel *label, NSString *stringCopied);
+@property(nonatomic, copy, nullable) void (^didCopyBlock)(QMUILabel *label, NSString *stringCopied);
@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILabel.m b/QMUI/QMUIKit/QMUIComponents/QMUILabel.m
index 6f423470..f50ddfad 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILabel.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILabel.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILabel.m
@@ -15,6 +15,7 @@
#import "QMUILabel.h"
#import "QMUICore.h"
+#import "UILabel+QMUI.h"
@interface QMUILabel ()
@@ -49,6 +50,33 @@ - (CGSize)intrinsicContentSize {
return [self sizeThatFits:CGSizeMake(preferredMaxLayoutWidth, CGFLOAT_MAX)];
}
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ if (self.truncatingTailView && self.attributedText.length) {
+ [self bringSubviewToFront:self.truncatingTailView];
+
+ // 不能通过修改 numberOfLines = 0 再恢复它的值,来计算高度是否折叠了,因为修改它的值会触发 layout,从而陷入死循环,所以这里只能通过 NSAttributedString 来计算内容的实际高度。注意如果 lineBreakMode 为 Tail 的话,NSAttributedString 必定只能计算单行的高度,所以要手动改为非 Tail 的值
+ CGSize limitSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), CGFLOAT_MAX);
+ NSMutableAttributedString *string = self.attributedText.mutableCopy;
+ if (self.numberOfLines != 1 && self.lineBreakMode == NSLineBreakByTruncatingTail) {
+ NSParagraphStyle *p = [string attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil];
+ if (p) {
+ NSMutableParagraphStyle *mutableP = p.mutableCopy;
+ mutableP.lineBreakMode = NSLineBreakByWordWrapping;
+ [string addAttribute:NSParagraphStyleAttributeName value:mutableP range:NSMakeRange(0, string.length)];
+ }
+ }
+ CGSize realSize = [string boundingRectWithSize:limitSize options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
+ BOOL shouldShowTruncatingTailView = realSize.height > CGRectGetHeight(self.bounds);
+ self.truncatingTailView.hidden = !shouldShowTruncatingTailView;
+ if (!self.truncatingTailView.hidden) {
+ CGFloat lineHeight = self.qmui_lineHeight;
+ [self.truncatingTailView sizeToFit];
+ self.truncatingTailView.frame = CGRectMake(CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - CGRectGetWidth(self.truncatingTailView.frame), CGRectGetHeight(self.bounds) - self.contentEdgeInsets.bottom - lineHeight, CGRectGetWidth(self.truncatingTailView.frame), lineHeight);
+ }
+ }
+}
+
- (void)drawTextInRect:(CGRect)rect {
rect = UIEdgeInsetsInsetRect(rect, self.contentEdgeInsets);
@@ -61,19 +89,39 @@ - (void)drawTextInRect:(CGRect)rect {
[super drawTextInRect:rect];
}
-- (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor {
- _highlightedBackgroundColor = highlightedBackgroundColor;
+- (void)setHighlighted:(BOOL)highlighted {
+ [super setHighlighted:highlighted];
- if (highlightedBackgroundColor) {
- self.originalBackgroundColor = self.backgroundColor;
+ if (self.highlightedBackgroundColor) {
+ [super setBackgroundColor:highlighted ? self.highlightedBackgroundColor : self.originalBackgroundColor];
}
}
-- (void)setHighlighted:(BOOL)highlighted {
- [super setHighlighted:highlighted];
+- (void)setBackgroundColor:(UIColor *)backgroundColor {
+ self.originalBackgroundColor = backgroundColor;
- if (self.highlightedBackgroundColor) {
- self.backgroundColor = highlighted ? self.highlightedBackgroundColor : self.originalBackgroundColor;
+ // 在出现 menu 的时候 backgroundColor 被修改,此时也不应该立马显示新的 backgroundColor
+ if (self.highlighted && self.highlightedBackgroundColor) {
+ return;
+ }
+
+ [super setBackgroundColor:backgroundColor];
+}
+
+// 当 label.highlighted = YES 时 backgroundColor 的 getter 会返回 self.highlightedBackgroundColor,因此如果在 highlighted = YES 时外部刚好执行了 `label.backgroundColor = label.backgroundColor` 就会导致 label 的背景色被错误地设置为高亮时的背景色,所以这里需要重写 getter 返回内部记录的 originalBackgroundColor
+- (UIColor *)backgroundColor {
+ return self.originalBackgroundColor;
+}
+
+#pragma mark - 自定义缩略点点点按钮
+
+- (void)setTruncatingTailView:(__kindof UIView *)truncatingTailView {
+ if (_truncatingTailView != truncatingTailView) {
+ [_truncatingTailView removeFromSuperview];
+ _truncatingTailView = truncatingTailView;
+ [self addSubview:_truncatingTailView];
+ _truncatingTailView.hidden = YES;
+ [self setNeedsLayout];
}
}
@@ -87,10 +135,6 @@ - (void)setCanPerformCopyAction:(BOOL)canPerformCopyAction {
[self addGestureRecognizer:self.longGestureRecognizer];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMenuWillHideNotification:) name:UIMenuControllerWillHideMenuNotification object:nil];
-
- if (!self.highlightedBackgroundColor) {
- self.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor; // 设置个默认值
- }
} else if (!_canPerformCopyAction && self.longGestureRecognizer) {
[self removeGestureRecognizer:self.longGestureRecognizer];
self.longGestureRecognizer = nil;
@@ -133,12 +177,11 @@ - (void)handleLongPressGestureRecognizer:(UIGestureRecognizer *)gestureRecognize
UIMenuController *menuController = [UIMenuController sharedMenuController];
UIMenuItem *copyMenuItem = [[UIMenuItem alloc] initWithTitle:self.menuItemTitleForCopyAction ?: @"复制" action:@selector(copyString:)];
[[UIMenuController sharedMenuController] setMenuItems:@[copyMenuItem]];
- [menuController setTargetRect:self.frame inView:self.superview];
- [menuController setMenuVisible:YES animated:YES];
+ [menuController showMenuFromView:self.superview rect:self.frame];
- [self setHighlighted:YES];
+ self.highlighted = YES;
} else if (gestureRecognizer.state == UIGestureRecognizerStatePossible) {
- [self setHighlighted:NO];
+ self.highlighted = NO;
}
}
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouter.h b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouter.h
new file mode 100644
index 00000000..391cbc8f
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouter.h
@@ -0,0 +1,19 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// QMUILayouter.h
+// QMUIKit
+//
+// Created by QMUI Team on 2024/1/2.
+//
+
+#import
+#import
+#import "QMUILayouterLinearHorizontal.h"
+#import "QMUILayouterLinearVertical.h"
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.h b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.h
new file mode 100644
index 00000000..7323fd28
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.h
@@ -0,0 +1,135 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// QMUILayouterItem.h
+// QMUIKit
+//
+// Created by QMUI Team on 2024/1/2.
+//
+
+#import
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSInteger, QMUILayouterAlignment) {
+ /// 对水平容器来说是从左往右,对竖直容器来说是从上往下。若容器大小不足以容纳所有 item,则末尾的 item 大小会被强制裁剪以保证不溢出。
+ QMUILayouterAlignmentLeading,
+
+ /// 对水平容器来说是从左往右然后整体右对齐父容器,对竖直容器来说是从上往下然后整体底对齐父容器。若 item 超过父容器大小,则与 QMUILayouterAlignmentLeading 一致。
+ QMUILayouterAlignmentTrailing,
+
+ /// 对水平容器来说是从左往右然后整体在父容器里居中,对竖直容器来说是从上往下然后整体在父容器里居中。若 item 超过父容器大小,则与 QMUILayouterAlignmentLeading 一致。
+ QMUILayouterAlignmentCenter,
+
+ /// 当表示与容器布局方向相同的方向时(例如 Linear 的水平,或 Vertical 的竖直),仅当子元素个数为1时有效,会在指定方向上撑满父容器。当子元素个数大于1时与 QMUILayouterAlignmentLeading 一致。
+ /// 当表示与容器布局方向垂直的方向时(例如 Linear 的竖直,或 Vertical 的水平),则所有子元素均会在指定方向上撑满父容器。
+ QMUILayouterAlignmentFill,
+};
+
+/// 表示父容器还有剩余空间时当前 item 也保持自身尺寸不变,不去拉伸填充剩余空间
+extern const CGFloat QMUILayouterGrowNever;
+
+/// 表示父容器还有剩余空间时当前 item 以最高优先级去填充(一般用1就行,不需要用到 Most)
+extern const CGFloat QMUILayouterGrowMost;
+
+/// 表示父容器空间不足以容纳所有 item,不得已要压缩 item 时,不要压缩当前 item
+extern const CGFloat QMUILayouterShrinkNever;
+
+/// 表示父容器空间不足以容纳所有 item,不得已要压缩 item 时,允许压缩当前 item(按各自尺寸比例)
+extern const CGFloat QMUILayouterShrinkDefault;
+
+/// 表示父容器空间不足以容纳所有 item,不得已要压缩 item 时,使当前 item 以最高优先级压缩
+extern const CGFloat QMUILayouterShrinkMost;
+
+@interface QMUILayouterItem : NSObject
+
+/// 通常用于生成一个子元素角色的 item,不允许拉伸也不允许缩放。
++ (instancetype)itemWithView:(__kindof UIView *)view margin:(UIEdgeInsets)margin;
+
+/// 通常用于生成一个子元素角色的 item
++ (instancetype)itemWithView:(__kindof UIView *)view margin:(UIEdgeInsets)margin grow:(CGFloat)grow shrink:(CGFloat)shrink;
+
+/// 关联的实体 view,如果当前 item 是虚拟布局容器,也可以不存在关联的实体 view。
+/// @note 一般将 view 添加到界面上后再赋值给这个属性,这样可确保后续的运算最准确。
+@property(nonatomic, weak, nullable) __kindof UIView *view;
+
+/// frame 的值变化时才会设置给 view 且标记为在下一次 runloop 里需要刷新布局。
+@property(nonatomic, assign) CGRect frame;
+
+/// 给 parentItem 布局自己时使用,自己内部 layout 时不使用,也不包含在自身的 sizeThatFits: 结果里。
+@property(nonatomic, assign) UIEdgeInsets margin;
+
+/// 表示父容器在布局自己时可忽略 item 自身的宽度,仅通过将所有 grow 大于0的 item 按各自 grow 比例计算得到宽度,例如一行里有两个 item,一个 item 宽度为自身内容宽度,另一个 item 撑满容器剩余空间。默认为 QMUILayouterGrowNever,也即自适应内容,设置为 QMUILayouterGrowMost 或某个大于0的数值可按比例撑满容器。
+/// @warning 仅在支持比例布局的容器里有效(例如 LinearHorizontal、LinearVertical)
+@property(nonatomic, assign) CGFloat grow;
+
+/// 当父容器空间不足以容纳所有 item 时,由每个 item 的 shrink 值及 item 的尺寸来决定该压缩哪个 item 的尺寸、压缩多少。默认为 QMUILayouterShrinkNever,值越大则压缩得越狠。
+@property(nonatomic, assign) CGFloat shrink;
+
+/// 最大的尺寸,在自身 sizeThatFits、父容器 grow 时生效,在 setFrame 时不限制(也即非要的话你也可以设置一个突破限制的尺寸),默认为 CGSizeMax
+@property(nonatomic, assign) CGSize maximumSize;
+
+/// 最小的尺寸,在自身 sizeThatFits、父容器 shrink 时生效,在 setFrame 时不限制(也即非要的话你也可以设置一个突破限制的尺寸),默认为 CGSizeZero
+@property(nonatomic, assign) CGSize minimumSize;
+
+/// 当前 item 是否可视,仅可视的 item 会参与布局运算。
+@property(nonatomic, assign, readonly) BOOL visible;
+
+/// 允许业务自定义 visible 的逻辑。
+@property(nonatomic, copy, nullable) BOOL (^visibleBlock)(QMUILayouterItem *aItem);
+
+/// 父容器,在 setChildItems: 时会将父子关系关联起来。
+@property(nonatomic, weak, readonly, nullable) __kindof QMUILayouterItem *parentItem;
+
+/// 所有子元素
+@property(nonatomic, strong) NSArray *childItems;
+
+/// 所有 visible 为 YES 的子元素,布局运算时使用这个。
+@property(nonatomic, weak, readonly, nullable) NSArray *visibleChildItems;
+
+// 便捷方法,会自动判空
+@property(nonatomic, weak, readonly, nullable) QMUILayouterItem *visibleChildItem0;
+@property(nonatomic, weak, readonly, nullable) QMUILayouterItem *visibleChildItem1;
+@property(nonatomic, weak, readonly, nullable) QMUILayouterItem *visibleChildItem2;
+@property(nonatomic, weak, readonly, nullable) QMUILayouterItem *visibleChildItem3;
+
+/// 计算在特定宽高下的自身尺寸,注意 self.margin 不参与其中。通常将 height 传 CGFLOAT_MAX 以得到一个自适应内容的大小。
+- (CGSize)sizeThatFits:(CGSize)size;
+
+/// 允许业务自定义 sizeThatFits: 的逻辑(注意这个主要用于父容器布局时询问子元素大小用,不用于元素计算自身内容大小时用),在调用完 block 后才进行 min/height 保护。
+@property(nonatomic, copy, nullable) CGSize (^sizeThatFitsBlock)(QMUILayouterItem *aItem, CGSize size, CGSize superResult);
+
+/// 保持 x/y 不变,将自身大小设置为不受宽高限制的尺寸,并将布局标记为需要被刷新。
+- (void)sizeToFit;
+
+/// 标记需要刷新布局,在同一个 runloop 里的所有 setNeedsLayout 会统一在下一个 runloop 里才一起布局。
+- (void)setNeedsLayout;
+
+/// 如果当前布局待刷新,则立即刷新,以便得到最新的布局结果。
+- (void)layoutIfNeeded;
+
+/// 是否在指定 view 的坐标系里显示自身及所有子元素的布局边框(颜色随机),请在 layoutSubviews、viewDidLayoutSubviews 里调用(也即每次参数 view 的布局发生变化时)。
+- (void)showDebugBorderRecursivelyInView:(UIView *)view;
+
+/// 一般用作调试时区分用,业务随意赋值。
+@property(nonatomic, copy, nullable) NSString *identifier;
+@end
+
+@interface QMUILayouterItem (UISubclassingHooks)
+
+/// 子类计算自身大小的逻辑请写在这个方法里,如果是外部希望得知当前元素的大小,请调用 sizeThatFits: 或 sizeToFit。
+/// @param shouldConsiderBlock 计算大小时是否需要考虑 sizeThatFitsBlock:,如果当前是外部询问元素大小,参数为 YES,如果是内部希望得知内容实际大小,参数为 NO。
+- (CGSize)sizeThatFits:(CGSize)size shouldConsiderBlock:(BOOL)shouldConsiderBlock;
+
+/// 子类重写布局时使用,外部不要直接调用它。可视情况自行决定是否要调用 super。
+- (void)layout;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.m b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.m
new file mode 100644
index 00000000..9dea640a
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.m
@@ -0,0 +1,286 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// QMUILayouterItem.m
+// QMUIKit
+//
+// Created by QMUI Team on 2024/1/2.
+//
+
+#import "QMUILayouterItem.h"
+#import "QMUICore.h"
+#import "NSArray+QMUI.h"
+#import "NSString+QMUI.h"
+#import "CALayer+QMUI.h"
+#import "UIColor+QMUI.h"
+
+const CGFloat QMUILayouterGrowNever = 0.0;
+const CGFloat QMUILayouterGrowMost = 99.0;
+const CGFloat QMUILayouterShrinkDefault = 1.0;
+const CGFloat QMUILayouterShrinkNever = 0.0;
+const CGFloat QMUILayouterShrinkMost = 99.0;
+
+@interface QMUILayouterItem ()
+@property(nonatomic, strong) CALayer *debugBorderLayer;
+@end
+
+@implementation QMUILayouterItem {
+ BOOL _shouldInvalidateLayout;
+}
+
++ (instancetype)itemWithView:(__kindof UIView *)view margin:(UIEdgeInsets)margin {
+ return [self itemWithView:view margin:margin grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkNever];
+}
+
++ (instancetype)itemWithView:(__kindof UIView *)view margin:(UIEdgeInsets)margin grow:(CGFloat)grow shrink:(CGFloat)shrink {
+ QMUILayouterItem *item = [[self alloc] init];
+ item.view = view;
+ item.margin = margin;
+ item.grow = grow;
+ item.shrink = shrink;
+ return item;
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _maximumSize = CGSizeMax;
+ _minimumSize = CGSizeZero;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ NSString * (^growName)(CGFloat grow) = ^NSString * (CGFloat grow) {
+ if (grow == QMUILayouterGrowNever) return @"Never";
+ if (grow == QMUILayouterGrowMost) return @"Most";
+ return [NSString stringWithFormat:@"%.1f", grow];
+ };
+ NSString * (^shrinkName)(CGFloat shrink) = ^NSString * (CGFloat shrink) {
+ if (shrink == QMUILayouterShrinkDefault) return @"Default";
+ if (shrink == QMUILayouterShrinkNever) return @"Never";
+ if (shrink == QMUILayouterShrinkMost) return @"Most";
+ return [NSString stringWithFormat:@"%.1f", shrink];
+ };
+ return [NSString qmui_stringByConcat:[super description], @", visible = ", StringFromBOOL(self.visible), @", frame = ", NSStringFromCGRect(self.frame), @", margin = ", NSStringFromUIEdgeInsets(self.margin), @", grow = ", growName(self.grow), @", shrink = ", shrinkName(self.shrink), (self.visibleChildItems.count ? [NSString stringWithFormat:@", visibleChild(%@)", @(self.visibleChildItems.count)] : @""), (self.view ? [NSString stringWithFormat:@", view = <%@: %p>", NSStringFromClass(self.view.class), self.view] : @""), nil];
+}
+
+@synthesize frame = _frame;
+- (void)setFrame:(CGRect)frame {
+ // QMUIViewSelfSizingHeight 的功能
+ if (isinf(frame.size.height)) {
+ if (frame.size.width > 0) {
+ CGFloat height = flat([self sizeThatFits:CGSizeMake(CGRectGetWidth(frame), CGFLOAT_MAX) shouldConsiderBlock:NO].height);
+ frame = CGRectSetHeight(frame, height);
+ } else {
+ frame.size.height = _frame.size.height;
+ }
+ }
+ BOOL frameChanged = !CGRectEqualToRect(self.frame, frame);
+ _frame = frame;
+ self.view.frame = frame;
+ if (frameChanged) {
+ [self setNeedsLayout];
+ }
+}
+
+- (CGRect)frame {
+ // 每个 item 不一定都存在 view,可能它只是一个虚拟的布局节点,所以这里要区分
+ if (self.view) {
+ return self.view.frame;
+ }
+ return _frame;
+}
+
+- (void)setView:(__kindof UIView *)view {
+ BOOL valueChanged = _view != view;
+ _view = view;
+ if (valueChanged) {
+ [self setNeedsLayout];
+ }
+}
+
+- (void)setMargin:(UIEdgeInsets)margin {
+ BOOL valueChanged = UIEdgeInsetsEqualToEdgeInsets(_margin, margin);
+ _margin = margin;
+ if (valueChanged) {
+ [self.parentItem setNeedsLayout];
+ }
+}
+
+- (void)setGrow:(CGFloat)grow {
+ NSAssert(grow >= 0, @"negative values are invalid for grow.");
+ grow = MAX(0, grow);
+ BOOL valueChanged = _grow != grow;
+ _grow = grow;
+ if (valueChanged) {
+ [self.parentItem setNeedsLayout];
+ }
+}
+
+- (void)setShrink:(CGFloat)shrink {
+ NSAssert(shrink >= 0, @"negative values are invalid for grow.");
+ shrink = MAX(0, shrink);
+ BOOL valueChanged = _shrink != shrink;
+ _shrink = shrink;
+ if (valueChanged) {
+ [self.parentItem setNeedsLayout];
+ }
+}
+
+- (BOOL)visible {
+ if (self.visibleBlock) return self.visibleBlock(self);
+ return self.view.superview && !self.view.hidden;
+}
+
+- (QMUILayouterItem *)visibleChildItem0 {
+ return [self visibleChildItemAtIndex:0];
+}
+
+- (QMUILayouterItem *)visibleChildItem1 {
+ return [self visibleChildItemAtIndex:1];
+}
+
+- (QMUILayouterItem *)visibleChildItem2 {
+ return [self visibleChildItemAtIndex:2];
+}
+
+- (QMUILayouterItem *)visibleChildItem3 {
+ return [self visibleChildItemAtIndex:3];
+}
+
+- (QMUILayouterItem *)visibleChildItemAtIndex:(NSUInteger)index {
+ return index < self.visibleChildItems.count ? self.visibleChildItems[index] : nil;
+}
+
+- (void)setChildItems:(NSArray *)childItems {
+ [_childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ obj->_parentItem = nil;
+ }];
+ _childItems = childItems;
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ obj->_parentItem = self;
+ }];
+}
+
+- (NSArray *)visibleChildItems {
+ return self.childItems.count ? [self.childItems qmui_filterWithBlock:^BOOL(QMUILayouterItem * _Nonnull item) {
+ return item.visible;
+ }] : nil;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+ return [self sizeThatFits:size shouldConsiderBlock:YES];
+}
+
+- (void)sizeToFit {
+ CGSize prefersSize = CGSizeMax;
+ // 参照系统 UILabel 的 sizeToFit 方式(在当前宽度下计算高度)
+ if ([self.view isKindOfClass:UILabel.class] && CGRectGetWidth(self.frame) > 0) {
+ prefersSize.width = CGRectGetWidth(self.frame);
+ }
+ CGSize size = [self sizeThatFits:prefersSize];
+ self.frame = CGRectSetSize(self.frame, size);
+}
+
+- (void)setNeedsLayout {
+ if (_shouldInvalidateLayout) return;
+ _shouldInvalidateLayout = YES;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (self->_shouldInvalidateLayout) {
+ [self layoutIfNeeded];
+ }
+ });
+}
+
+- (void)layoutIfNeeded {
+ [self layout];
+ [self layoutDebugBorderLayer];
+ _shouldInvalidateLayout = NO;
+}
+
+- (CALayer *)generateDebugBorderLayerContainer {
+ CALayer *layer = CALayer.layer;
+ layer.name = @"QMUILayouterDebugBorderLayerContainer";
+ [layer qmui_removeDefaultAnimations];
+ return layer;
+}
+
+- (CALayer *)generateDebugBorderLayer {
+ CALayer *layer = CALayer.layer;
+ layer.name = @"QMUILayouterDebugBorderLayer";
+ [layer qmui_removeDefaultAnimations];
+ UIColor *color = UIColor.qmui_randomColor;
+ layer.backgroundColor = [color colorWithAlphaComponent:.1].CGColor;
+ layer.borderColor = color.CGColor;
+ layer.borderWidth = 1;
+ return layer;
+}
+
+- (void)showDebugBorderRecursivelyInView:(UIView *)view {
+ if (!view) return;
+ CALayer *container = [view.layer.sublayers qmui_firstMatchWithBlock:^BOOL(__kindof CALayer * _Nonnull item) {
+ return [item.name isEqualToString:@"QMUILayouterDebugBorderLayerContainer"];
+ }];
+ if (!container) {
+ container = [self generateDebugBorderLayerContainer];
+ [view.layer addSublayer:container];
+ }
+ [container.sublayers.copy enumerateObjectsUsingBlock:^(__kindof CALayer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ if ([obj.name isEqualToString:@"QMUILayouterDebugBorderLayer"]) [obj removeFromSuperlayer];
+ }];
+ container.frame = view.bounds;
+ [self showDebugBorderInContainer:container];
+ [self.childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ [obj showDebugBorderInContainer:container];
+ }];
+}
+
+- (void)showDebugBorderInContainer:(CALayer *)container {
+ if (!container) return;
+ if (!self.debugBorderLayer) {
+ self.debugBorderLayer = [self generateDebugBorderLayer];
+ [container addSublayer:self.debugBorderLayer];
+ } else if (self.debugBorderLayer.superlayer != container) {
+ [self.debugBorderLayer removeFromSuperlayer];
+ [container addSublayer:self.debugBorderLayer];
+ }
+}
+
+- (void)layoutDebugBorderLayer {
+ if (!self.debugBorderLayer || !self.debugBorderLayer.superlayer) return;
+ if (self.view) {
+ UIView *containerView = (UIView *)self.debugBorderLayer.superlayer.superlayer.delegate;
+ CGRect frame = [self.view convertRect:self.view.bounds toView:containerView];
+ self.debugBorderLayer.frame = frame;
+ } else {
+ self.debugBorderLayer.frame = self.frame;
+ }
+}
+
+@end
+
+@implementation QMUILayouterItem (UISubclassingHooks)
+
+- (void)layout {
+}
+
+- (CGSize)sizeThatFits:(CGSize)size shouldConsiderBlock:(BOOL)shouldConsiderBlock {
+ if (CGSizeEqualToSize(self.view.bounds.size, size) || CGSizeIsEmpty(size)) {
+ size = CGSizeMax;
+ }
+ CGSize result = [self.view sizeThatFits:size];
+ if (shouldConsiderBlock && self.sizeThatFitsBlock) {
+ result = self.sizeThatFitsBlock(self, size, result);
+ }
+ result.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, result.width));
+ result.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, result.height));
+ return result;
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.h b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.h
new file mode 100644
index 00000000..98e3655d
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.h
@@ -0,0 +1,46 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// QMUILayouterLinearHorizontal.h
+// QMUIKit
+//
+// Created by QMUI Team on 2024/1/2.
+//
+
+#import
+#import
+#import "QMUILayouterItem.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ 水平方向的线性布局,若容器大小不足以容纳所有 item,则末尾的 item 大小会被强制裁剪以保证不溢出。
+ 子元素可通过设置自己的 grow 来达到撑满容器的效果。
+ */
+@interface QMUILayouterLinearHorizontal : QMUILayouterItem
+
++ (instancetype)itemWithChildItems:(NSArray *)childItems
+ spacingBetweenItems:(CGFloat)spacingBetweenItems;
+
++ (instancetype)itemWithChildItems:(NSArray *)childItems
+ spacingBetweenItems:(CGFloat)spacingBetweenItems
+ horizontal:(QMUILayouterAlignment)horizontal
+ vertical:(QMUILayouterAlignment)vertical;
+
+/// 子元素之间的间距
+@property(nonatomic, assign) CGFloat spacingBetweenItems;
+
+/// 子元素水平方向上的布局方式,默认为 QMUILayouterAlignmentLeading,每种 enum 的布局说明请查看 enum 定义。
+@property(nonatomic, assign) QMUILayouterAlignment childHorizontalAlignment;
+
+/// 子元素竖直方向上的布局方式,默认为 QMUILayouterAlignmentLeading,每种 enum 的布局说明请查看 enum 定义。
+@property(nonatomic, assign) QMUILayouterAlignment childVerticalAlignment;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.m b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.m
new file mode 100644
index 00000000..ecedf972
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.m
@@ -0,0 +1,189 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// QMUILayouterLinearHorizontal.m
+// QMUIKit
+//
+// Created by QMUI Team on 2024/1/2.
+//
+
+#import "QMUILayouterLinearHorizontal.h"
+#import "QMUICore.h"
+#import "NSArray+QMUI.h"
+#import "UIView+QMUI.h"
+
+@implementation QMUILayouterLinearHorizontal
+
++ (instancetype)itemWithChildItems:(NSArray *)childItems spacingBetweenItems:(CGFloat)spacingBetweenItems {
+ return [self itemWithChildItems:childItems spacingBetweenItems:spacingBetweenItems horizontal:QMUILayouterAlignmentLeading vertical:QMUILayouterAlignmentLeading];
+}
+
++ (instancetype)itemWithChildItems:(NSArray *)childItems spacingBetweenItems:(CGFloat)spacingBetweenItems horizontal:(QMUILayouterAlignment)horizontal vertical:(QMUILayouterAlignment)vertical {
+ QMUILayouterLinearHorizontal *item = [[self alloc] init];
+ item.childItems = childItems;
+ item.spacingBetweenItems = spacingBetweenItems;
+ item.childHorizontalAlignment = horizontal;
+ item.childVerticalAlignment = vertical;
+ return item;
+}
+
+- (NSString *)description {
+ NSString * (^alignmentName)(QMUILayouterAlignment alignment) = ^NSString *(QMUILayouterAlignment alignment) {
+ return @[@"Leading", @"Trailing", @"Center", @"Fill"][alignment];
+ };
+ return [NSString qmui_stringByConcat:[super description], @", horizontal = ", alignmentName(self.childHorizontalAlignment), @", vertical = ", alignmentName(self.childVerticalAlignment), nil];
+}
+
+// 容器性质的 layouter,不存在关联的实体 view,则始终认为是可视的,如果是 parentItem 的 parentItem 不可见,则由 parentItem 自己去管
+- (BOOL)visible {
+ if (self.visibleBlock) return self.visibleBlock(self);
+ return self.visibleChildItems.count;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size shouldConsiderBlock:(BOOL)shouldConsiderBlock {
+ NSArray *childItems = self.visibleChildItems;
+ if (!childItems.count) return self.minimumSize;
+ if (CGSizeEqualToSize(self.frame.size, size) || CGSizeIsEmpty(size)) {
+ size = CGSizeMax;
+ }
+ __block CGSize contentSize = CGSizeZero;
+ __block CGFloat totalShrink = QMUILayouterShrinkNever;
+ __block NSMutableDictionary *cachedSize = NSMutableDictionary.new;
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ CGSize s = [obj sizeThatFits:CGSizeMax];
+ cachedSize[[NSString stringWithFormat:@"%p", obj]] = [NSValue valueWithCGSize:s];
+ contentSize.width += s.width + UIEdgeInsetsGetHorizontalValue(obj.margin) + self.spacingBetweenItems;
+ contentSize.height = MAX(contentSize.height, s.height + UIEdgeInsetsGetVerticalValue(obj.margin));
+
+ if (obj.shrink > QMUILayouterShrinkNever) {
+ totalShrink += s.width * obj.shrink;
+ }
+ }];
+ contentSize.width -= self.spacingBetweenItems;
+ if (contentSize.width <= size.width || totalShrink == QMUILayouterShrinkNever) {
+ if (shouldConsiderBlock && self.sizeThatFitsBlock) {
+ contentSize = self.sizeThatFitsBlock(self, size, contentSize);
+ }
+ contentSize.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, contentSize.width));
+ contentSize.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, contentSize.height));
+ return contentSize;
+ }
+
+ __block CGSize resultSize = CGSizeZero;
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ CGSize s = cachedSize[[NSString stringWithFormat:@"%p", obj]].CGSizeValue;
+ if (obj.shrink > QMUILayouterGrowNever) {
+ CGFloat spaceToShrink = contentSize.width - size.width;
+ CGFloat w = s.width - spaceToShrink * s.width * obj.shrink / totalShrink;
+ CGFloat h = [obj sizeThatFits:CGSizeMake(w, CGFLOAT_MAX)].height;
+ s.width = w;
+ s.height = h;
+ }
+ resultSize.width += s.width + UIEdgeInsetsGetHorizontalValue(obj.margin) + self.spacingBetweenItems;
+ resultSize.height = MAX(resultSize.height, s.height + UIEdgeInsetsGetVerticalValue(obj.margin));
+ }];
+ resultSize.width -= self.spacingBetweenItems;
+ if (shouldConsiderBlock && self.sizeThatFitsBlock) {
+ resultSize = self.sizeThatFitsBlock(self, size, resultSize);
+ }
+ resultSize.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, resultSize.width));
+ resultSize.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, resultSize.height));
+ return resultSize;
+}
+
+- (void)layout {
+ NSArray *childItems = self.visibleChildItems;
+ CGSize contentSize = [self sizeThatFits:CGSizeMax shouldConsiderBlock:NO];
+
+ __block CGFloat totalGrow = QMUILayouterGrowNever;
+ __block CGFloat spaceToGrow = CGRectGetWidth(self.frame);// 父容器里待填充的多余空间(容器总大小减去所有固定的值,包括 spacingBetweenItems、所有 item 的 margin 区域、grow = Never 的 item 的 width之后,剩下的空间)
+ __block CGFloat totalShrink = QMUILayouterShrinkNever;
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ [obj sizeToFit];
+
+ spaceToGrow -= CGRectGetWidth(obj.frame) + UIEdgeInsetsGetHorizontalValue(obj.margin) + self.spacingBetweenItems;
+ if (obj.grow > QMUILayouterGrowNever) {
+ totalGrow += obj.grow;
+ }
+
+ if (obj.shrink > QMUILayouterShrinkNever) {
+ totalShrink += CGRectGetWidth(obj.frame) * obj.shrink;
+ }
+ }];
+ spaceToGrow += self.spacingBetweenItems;
+ BOOL shouldCalcGrow = totalGrow > QMUILayouterGrowNever && contentSize.width < CGRectGetWidth(self.frame);
+ BOOL shouldCalcShrink = totalShrink > QMUILayouterShrinkNever && contentSize.width > CGRectGetWidth(self.frame);
+
+ __block CGFloat minX = CGRectGetMinX(self.frame);
+ __block CGFloat minY = CGRectGetMinY(self.frame);
+ __block CGFloat maxX = CGRectGetMaxX(self.frame);
+ __block CGFloat maxY = CGRectGetMaxY(self.frame);
+ QMUILayouterAlignment childHorizontalAlignment = self.childHorizontalAlignment;
+ QMUILayouterAlignment childVerticalAlignment = self.childVerticalAlignment;
+
+ // 不需要考虑 grow/shrink 的情况,先把 minX 算出来
+ if (!shouldCalcGrow && !shouldCalcShrink && childHorizontalAlignment != QMUILayouterAlignmentLeading) {
+ if (contentSize.width >= CGRectGetWidth(self.frame)) {
+ // 不管哪种布局方式,只要内容超过容器,统一按 Leading 处理
+ childHorizontalAlignment = QMUILayouterAlignmentLeading;
+ } else if (childHorizontalAlignment == QMUILayouterAlignmentTrailing) {
+ minX = MAX(minX, CGRectGetMaxX(self.frame) - contentSize.width);
+ childHorizontalAlignment = QMUILayouterAlignmentLeading;
+ } else if (childHorizontalAlignment == QMUILayouterAlignmentCenter) {
+ minX = MAX(minX, minX + CGFloatGetCenter(CGRectGetWidth(self.frame), contentSize.width));
+ childHorizontalAlignment = QMUILayouterAlignmentLeading;
+ } else if (childHorizontalAlignment == QMUILayouterAlignmentFill) {
+ if (childItems.count > 1) {
+ // 与容器相同方向的 Fill 仅在只有一个子元素时有效,超过一个子元素则视为 Leading
+ // 如果你希望多个 childItem 可拉伸铺满,应该用 childItem.grow 来控制,而不是用 Fill
+ childHorizontalAlignment = QMUILayouterAlignmentLeading;
+ } else {
+ // 一个子元素的情况,直接布局掉算了
+ QMUILayouterItem *obj = self.visibleChildItem0;
+ obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left);
+ obj.frame = CGRectSetWidth(obj.frame, maxX - obj.margin.right - CGRectGetMinX(obj.frame));
+ }
+ }
+ }
+
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left);
+ if (shouldCalcGrow && obj.grow > QMUILayouterGrowNever) {
+ CGFloat w = CGRectGetWidth(obj.frame) + spaceToGrow * obj.grow / totalGrow;
+ obj.frame = CGRectSetSize(obj.frame, CGSizeMake(w, QMUIViewSelfSizingHeight));
+ }
+ if (shouldCalcShrink && obj.shrink > QMUILayouterGrowNever) {
+ CGFloat spaceToShrink = contentSize.width - CGRectGetWidth(self.frame);
+ CGFloat w = CGRectGetWidth(obj.frame) - spaceToShrink * CGRectGetWidth(obj.frame) * obj.shrink / totalShrink;
+ w = MAX(0, w);
+ obj.frame = CGRectSetSize(obj.frame, CGSizeMake(w, QMUIViewSelfSizingHeight));
+ obj.frame = CGRectSetHeight(obj.frame, MIN(CGRectGetHeight(self.frame) - UIEdgeInsetsGetVerticalValue(obj.margin), CGRectGetHeight(obj.frame)));
+ }
+ if (CGRectGetMaxX(obj.frame) + obj.margin.right > maxX) {
+ obj.frame = CGRectSetWidth(obj.frame, maxX - obj.margin.right - CGRectGetMinX(obj.frame));
+ }
+
+ minX = CGRectGetMaxX(obj.frame) + obj.margin.right + self.spacingBetweenItems;
+
+ if (childVerticalAlignment == QMUILayouterAlignmentTrailing) {
+ obj.frame = CGRectSetY(obj.frame, maxY - obj.margin.bottom - CGRectGetHeight(obj.frame));
+ } else if (childVerticalAlignment == QMUILayouterAlignmentCenter) {
+ obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top + CGFloatGetCenter(maxY - minY - UIEdgeInsetsGetVerticalValue(obj.margin), CGRectGetHeight(obj.frame)));
+ } else if (childVerticalAlignment == QMUILayouterAlignmentFill) {
+ obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top);
+ obj.frame = CGRectSetHeight(obj.frame, maxY - obj.margin.bottom - CGRectGetMinY(obj.frame));
+ } else {
+ obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top);
+ }
+
+ [obj layoutIfNeeded];
+ }];
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.h b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.h
new file mode 100644
index 00000000..95ab8e70
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.h
@@ -0,0 +1,46 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// QMUILayouterLinearVertical.h
+// QMUIKit
+//
+// Created by QMUI Team on 2024/1/2.
+//
+
+#import
+#import
+#import "QMUILayouterItem.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ 竖直方向的线性布局,若容器大小不足以容纳所有 item,则末尾的 item 大小会被强制裁剪以保证不溢出。
+ 子元素可通过设置自己的 grow 来达到撑满容器的效果。
+ */
+@interface QMUILayouterLinearVertical : QMUILayouterItem
+
++ (instancetype)itemWithChildItems:(NSArray *)childItems
+ spacingBetweenItems:(CGFloat)spacingBetweenItems;
+
++ (instancetype)itemWithChildItems:(NSArray *)childItems
+ spacingBetweenItems:(CGFloat)spacingBetweenItems
+ horizontal:(QMUILayouterAlignment)horizontal
+ vertical:(QMUILayouterAlignment)vertical;
+
+/// 子元素之间的间距
+@property(nonatomic, assign) CGFloat spacingBetweenItems;
+
+/// 子元素水平方向上的布局方式,默认为 QMUILayouterAlignmentLeading,每种 enum 的布局说明请查看 enum 定义。
+@property(nonatomic, assign) QMUILayouterAlignment childHorizontalAlignment;
+
+/// 子元素竖直方向上的布局方式,默认为 QMUILayouterAlignmentLeading,每种 enum 的布局说明请查看 enum 定义。
+@property(nonatomic, assign) QMUILayouterAlignment childVerticalAlignment;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.m b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.m
new file mode 100644
index 00000000..5e66320d
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.m
@@ -0,0 +1,187 @@
+/**
+ * Tencent is pleased to support the open source community by making QMUI_iOS available.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+//
+// QMUILayouterLinearVertical.m
+// QMUIKit
+//
+// Created by QMUI Team on 2024/1/2.
+//
+
+#import "QMUILayouterLinearVertical.h"
+#import "QMUICore.h"
+#import "NSString+QMUI.h"
+
+@implementation QMUILayouterLinearVertical
+
++ (instancetype)itemWithChildItems:(NSArray *)childItems spacingBetweenItems:(CGFloat)spacingBetweenItems {
+ return [self itemWithChildItems:childItems spacingBetweenItems:spacingBetweenItems horizontal:QMUILayouterAlignmentLeading vertical:QMUILayouterAlignmentLeading];
+}
+
++ (instancetype)itemWithChildItems:(NSArray *)childItems spacingBetweenItems:(CGFloat)spacingBetweenItems horizontal:(QMUILayouterAlignment)horizontal vertical:(QMUILayouterAlignment)vertical {
+ QMUILayouterLinearVertical *item = [[self alloc] init];
+ item.childItems = childItems;
+ item.spacingBetweenItems = spacingBetweenItems;
+ item.childHorizontalAlignment = horizontal;
+ item.childVerticalAlignment = vertical;
+ return item;
+}
+
+- (NSString *)description {
+ NSString * (^alignmentName)(QMUILayouterAlignment alignment) = ^NSString *(QMUILayouterAlignment alignment) {
+ return @[@"Leading", @"Trailing", @"Center", @"Fill"][alignment];
+ };
+ return [NSString qmui_stringByConcat:[super description], @", horizontal = ", alignmentName(self.childHorizontalAlignment), @", vertical = ", alignmentName(self.childVerticalAlignment), nil];
+}
+
+// 容器性质的 layouter,不存在关联的实体 view,则始终认为是可视的,如果是 parentItem 的 parentItem 不可见,则由 parentItem 自己去管
+- (BOOL)visible {
+ if (self.visibleBlock) return self.visibleBlock(self);
+ return self.visibleChildItems.count;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size shouldConsiderBlock:(BOOL)shouldConsiderBlock {
+ NSArray *childItems = self.visibleChildItems;
+ if (!childItems.count) return self.minimumSize;
+ if (CGSizeEqualToSize(self.frame.size, size) || CGSizeIsEmpty(size)) {
+ size = CGSizeMax;
+ }
+ __block CGSize contentSize = CGSizeZero;
+ __block CGFloat totalShrink = QMUILayouterShrinkNever;
+ __block NSMutableDictionary *cachedSize = NSMutableDictionary.new;
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ CGSize s = [obj sizeThatFits:CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(obj.margin), CGFLOAT_MAX)];
+ cachedSize[[NSString stringWithFormat:@"%p", obj]] = [NSValue valueWithCGSize:s];
+ contentSize.width = MAX(contentSize.width, s.width + UIEdgeInsetsGetHorizontalValue(obj.margin));
+ contentSize.height += s.height + UIEdgeInsetsGetVerticalValue(obj.margin) + self.spacingBetweenItems;
+
+ if (obj.shrink > QMUILayouterShrinkNever) {
+ totalShrink += s.height * obj.shrink;
+ }
+ }];
+ contentSize.height -= self.spacingBetweenItems;
+ if (contentSize.height <= size.height || totalShrink == QMUILayouterShrinkNever) {
+ if (shouldConsiderBlock && self.sizeThatFitsBlock) {
+ contentSize = self.sizeThatFitsBlock(self, size, contentSize);
+ }
+ contentSize.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, contentSize.width));
+ contentSize.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, contentSize.height));
+ return contentSize;
+ }
+
+ __block CGSize resultSize = CGSizeZero;
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ CGSize s = cachedSize[[NSString stringWithFormat:@"%p", obj]].CGSizeValue;
+ if (obj.shrink > QMUILayouterShrinkNever) {
+ CGFloat spaceToShrink = contentSize.height - size.height;
+ CGFloat h = s.height - spaceToShrink * s.height * obj.shrink / totalShrink;
+ s.height = h;
+ }
+ resultSize.width = MAX(resultSize.width, s.width + UIEdgeInsetsGetHorizontalValue(obj.margin));
+ resultSize.height += s.height + UIEdgeInsetsGetVerticalValue(obj.margin) + self.spacingBetweenItems;
+ }];
+ resultSize.height -= self.spacingBetweenItems;
+ if (shouldConsiderBlock && self.sizeThatFitsBlock) {
+ resultSize = self.sizeThatFitsBlock(self, size, resultSize);
+ }
+ resultSize.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, resultSize.width));
+ resultSize.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, resultSize.height));
+ return resultSize;
+}
+
+- (void)layout {
+ NSArray *childItems = self.visibleChildItems;
+ CGSize contentSize = [self sizeThatFits:CGSizeMake(CGRectGetWidth(self.frame), CGFLOAT_MAX) shouldConsiderBlock:NO];
+ __block CGFloat totalGrow = QMUILayouterGrowNever;
+ __block CGFloat spaceToGrow = CGRectGetHeight(self.frame);// 父容器里待填充的多余空间(容器总大小减去所有固定的值,包括 spacingBetweenItems、所有 item 的 margin 区域、grow = Never 的 item 的 width之后,剩下的空间)
+ __block CGFloat totalShrink = QMUILayouterShrinkNever;
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ CGFloat itemMaxWidth = CGRectGetWidth(self.frame) - UIEdgeInsetsGetHorizontalValue(obj.margin);
+ CGSize itemSize = [obj sizeThatFits:CGSizeMake(itemMaxWidth, CGFLOAT_MAX)];
+ itemSize.width = MIN(itemMaxWidth, itemSize.width);
+ obj.frame = CGRectSetSize(obj.frame, itemSize);
+
+ spaceToGrow -= CGRectGetHeight(obj.frame) + UIEdgeInsetsGetVerticalValue(obj.margin) + self.spacingBetweenItems;
+ if (obj.grow > QMUILayouterGrowNever) {
+ totalGrow += obj.grow;
+ }
+
+ if (obj.shrink > QMUILayouterShrinkNever) {
+ totalShrink += CGRectGetHeight(obj.frame) * obj.shrink;
+ }
+ }];
+ spaceToGrow += self.spacingBetweenItems;
+ BOOL shouldCalcGrow = totalGrow > QMUILayouterGrowNever && contentSize.height < CGRectGetHeight(self.frame);
+ BOOL shouldCalcShrink = totalShrink > QMUILayouterShrinkNever && contentSize.height > CGRectGetHeight(self.frame);
+
+ __block CGFloat minX = CGRectGetMinX(self.frame);
+ __block CGFloat minY = CGRectGetMinY(self.frame);
+ __block CGFloat maxX = CGRectGetMaxX(self.frame);
+ __block CGFloat maxY = CGRectGetMaxY(self.frame);
+ QMUILayouterAlignment childVerticalAlignment = self.childVerticalAlignment;
+ QMUILayouterAlignment childHorizontalAlignment = self.childHorizontalAlignment;
+
+ // 不需要考虑 grow/shrink 的情况,先把 minX 算出来
+ if (!shouldCalcGrow && !shouldCalcShrink && childVerticalAlignment != QMUILayouterAlignmentLeading) {
+ if (contentSize.height >= CGRectGetHeight(self.frame)) {
+ // 不管哪种布局方式,只要内容超过容器,统一按 Leading 处理
+ childVerticalAlignment = QMUILayouterAlignmentLeading;
+ } else if (childVerticalAlignment == QMUILayouterAlignmentTrailing) {
+ minY = MAX(minY, CGRectGetMaxY(self.frame) - contentSize.height);
+ childVerticalAlignment = QMUILayouterAlignmentLeading;
+ } else if (childVerticalAlignment == QMUILayouterAlignmentCenter) {
+ minY = MAX(minY, minY + CGFloatGetCenter(CGRectGetHeight(self.frame), contentSize.height));
+ childVerticalAlignment = QMUILayouterAlignmentLeading;
+ } else if (childVerticalAlignment == QMUILayouterAlignmentFill) {
+ if (childItems.count > 1) {
+ // 与容器相同方向的 Fill 仅在只有一个子元素时有效,超过一个子元素则视为 Leading
+ // 如果你希望多个 childItem 可拉伸铺满,应该用 childItem.grow 来控制,而不是用 Fill
+ childVerticalAlignment = QMUILayouterAlignmentLeading;
+ } else {
+ // 一个子元素的情况,直接布局掉算了
+ QMUILayouterItem *obj = self.visibleChildItem0;
+ obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top);
+ obj.frame = CGRectSetHeight(obj.frame, maxY - obj.margin.bottom - CGRectGetMinY(obj.frame));
+ }
+ }
+ }
+
+ [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top);
+ if (shouldCalcGrow && obj.grow > QMUILayouterGrowNever) {
+ CGFloat h = CGRectGetHeight(obj.frame) + spaceToGrow * obj.grow / totalGrow;
+ obj.frame = CGRectSetHeight(obj.frame, h);
+ }
+ if (shouldCalcShrink && obj.shrink > QMUILayouterShrinkNever) {
+ CGFloat spaceToShrink = contentSize.height - CGRectGetHeight(self.frame);
+ CGFloat h = CGRectGetHeight(obj.frame) - spaceToShrink * CGRectGetHeight(obj.frame) * obj.shrink / totalShrink;
+ h = MAX(0, h);
+ obj.frame = CGRectSetHeight(obj.frame, h);
+ }
+ if (CGRectGetMaxY(obj.frame) + obj.margin.bottom > maxY) {
+ obj.frame = CGRectSetHeight(obj.frame, maxY - obj.margin.bottom - CGRectGetMinY(obj.frame));
+ }
+
+ minY = CGRectGetMaxY(obj.frame) + obj.margin.bottom + self.spacingBetweenItems;
+
+ if (childHorizontalAlignment == QMUILayouterAlignmentTrailing) {
+ obj.frame = CGRectSetX(obj.frame, maxX - obj.margin.right - CGRectGetWidth(obj.frame));
+ } else if (childHorizontalAlignment == QMUILayouterAlignmentCenter) {
+ obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left + CGFloatGetCenter(maxX - minX - UIEdgeInsetsGetHorizontalValue(obj.margin), CGRectGetWidth(obj.frame)));
+ } else if (childHorizontalAlignment == QMUILayouterAlignmentFill) {
+ obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left);
+ obj.frame = CGRectSetWidth(obj.frame, maxX - obj.margin.right - CGRectGetMinX(obj.frame));
+ } else {
+ obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left);
+ }
+
+ [obj layoutIfNeeded];
+ }];
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.h
index c2a83450..ea46e130 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILog.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h
index 086d051f..3455dbf5 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogItem.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m
index a0135423..82fa943d 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogItem.m
@@ -20,7 +20,7 @@
@implementation QMUILogItem
+ (instancetype)logItemWithLevel:(QMUILogLevel)level name:(NSString *)name logString:(NSString *)logString, ... {
- QMUILogItem *logItem = [[QMUILogItem alloc] init];
+ QMUILogItem *logItem = [[self alloc] init];
logItem.level = level;
logItem.name = name;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h
index 6808e1c6..918ee55e 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogNameManager.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m
index 614ebcd0..972c14bd 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogNameManager.m
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h
index d47c86d0..1959beb7 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogger.h
// QMUIKit
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m
index b6eaf534..71e52051 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogger.m
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h
index be6b35be..bf04276d 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogManagerViewController.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m
index 0d09fbbe..dead13a4 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogManagerViewController.m
@@ -25,13 +25,13 @@
#import "QMUITableViewCell.h"
#import "QMUISearchController.h"
#import "UIBarItem+QMUI.h"
+#import "UIViewController+QMUI.h"
@interface QMUILogManagerViewController ()
@property(nonatomic, copy) NSDictionary *allNames;
@property(nonatomic, copy) NSArray *sortedLogNames;
@property(nonatomic, copy) NSArray *sectionIndexTitles;
-@property(nonatomic, assign) UIStatusBarStyle statusBarStyle;
@end
@implementation QMUILogManagerViewController
@@ -39,7 +39,6 @@ @implementation QMUILogManagerViewController
- (void)didInitializeWithStyle:(UITableViewStyle)style {
[super didInitializeWithStyle:style];
self.rowCountWhenShowSearchBar = 10;
- self.statusBarStyle = QMUICMIActivated ? (StatusbarStyleLightInitially ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault) : UIStatusBarStyleDefault;
}
- (void)initTableView {
@@ -47,6 +46,13 @@ - (void)initTableView {
[self setupDataSource];
}
+- (void)initSearchController {
+ [super initSearchController];
+ self.searchController.qmui_preferredStatusBarStyleBlock = ^UIStatusBarStyle{
+ return UIStatusBarStyleDarkContent;
+ };
+}
+
- (void)viewDidLoad {
[super viewDidLoad];
[self checkEmptyView];
@@ -61,10 +67,6 @@ - (void)setupNavigationItems {
}
}
-- (UIStatusBarStyle)preferredStatusBarStyle {
- return self.statusBarStyle;
-}
-
- (void)setupDataSource {
self.allNames = [QMUILogger sharedInstance].logNameManager.allNames;
@@ -166,25 +168,25 @@ - (void)handleMenuItemEvent {
menuView.maximumWidth = 124;
menuView.safetyMarginsOfSuperview = UIEdgeInsetsSetRight(menuView.safetyMarginsOfSuperview, 6);
menuView.items = @[
- [QMUIPopupMenuButtonItem itemWithImage:nil title:@"开启全部" handler:^(QMUIPopupMenuButtonItem *aItem) {
- for (NSString *logName in self.allNames) {
- [[QMUILogger sharedInstance].logNameManager setEnabled:YES forLogName:logName];
- }
- [self reloadData];
- [aItem.menuView hideWithAnimated:YES];
- }],
- [QMUIPopupMenuButtonItem itemWithImage:nil title:@"禁用全部" handler:^(QMUIPopupMenuButtonItem *aItem) {
- for (NSString *logName in self.allNames) {
- [[QMUILogger sharedInstance].logNameManager setEnabled:NO forLogName:logName];
- }
- [self reloadData];
- [aItem.menuView hideWithAnimated:YES];
- }],
- [QMUIPopupMenuButtonItem itemWithImage:nil title:@"清空全部" handler:^(QMUIPopupMenuButtonItem *aItem) {
- [[QMUILogger sharedInstance].logNameManager removeAllNames];
- [self reloadData];
- [aItem.menuView hideWithAnimated:YES];
- }]];
+ [QMUIPopupMenuItem itemWithTitle:@"开启全部" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) {
+ for (NSString *logName in self.allNames) {
+ [[QMUILogger sharedInstance].logNameManager setEnabled:YES forLogName:logName];
+ }
+ [self reloadData];
+ [aItem.menuView hideWithAnimated:YES];
+ }],
+ [QMUIPopupMenuItem itemWithTitle:@"禁用全部" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) {
+ for (NSString *logName in self.allNames) {
+ [[QMUILogger sharedInstance].logNameManager setEnabled:NO forLogName:logName];
+ }
+ [self reloadData];
+ [aItem.menuView hideWithAnimated:YES];
+ }],
+ [QMUIPopupMenuItem itemWithTitle:@"清空全部" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) {
+ [[QMUILogger sharedInstance].logNameManager removeAllNames];
+ [self reloadData];
+ [aItem.menuView hideWithAnimated:YES];
+ }]];
menuView.sourceBarItem = self.navigationItem.rightBarButtonItem;
[menuView showWithAnimated:YES];
}
@@ -247,17 +249,9 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF
}
}
-- (void)willPresentSearchController:(QMUISearchController *)searchController {
- self.statusBarStyle = UIStatusBarStyleDefault;
- [self setNeedsStatusBarAppearanceUpdate];
-}
-
- (void)willDismissSearchController:(QMUISearchController *)searchController {
-
// 在搜索状态里可能修改了 switch 的值,则退出时强制刷新一下默认状态的列表
[self reloadData];
- self.statusBarStyle = QMUICMIActivated ? (StatusbarStyleLightInitially ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault) : UIStatusBarStyleDefault;
- [self setNeedsStatusBarAppearanceUpdate];
}
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h
index e448eefc..8fc9b904 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogger+QMUIConfigurationTemplate.h
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m
index ba2d22ed..04a54869 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUILogger+QMUIConfigurationTemplate.m
@@ -23,13 +23,10 @@ + (void)load {
dispatch_once(&onceToken, ^{
OverrideImplementation([QMUILogger class], @selector(printLogWithFile:line:func:logItem:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(QMUILogger *selfObject, const char *file, int line, const char *func, QMUILogItem *logItem) {
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- // 不同级别的 log 可通过配置表的开关来控制是否要输出
- if (logItem.level == QMUILogLevelDefault && !ShouldPrintDefaultLog) return;
- if (logItem.level == QMUILogLevelInfo && !ShouldPrintInfoLog) return;
- if (logItem.level == QMUILogLevelWarn && !ShouldPrintWarnLog) return;
- }
+ // 不同级别的 log 可通过配置表的开关来控制是否要输出
+ if (logItem.level == QMUILogLevelDefault && !ShouldPrintDefaultLog) return;
+ if (logItem.level == QMUILogLevelInfo && !ShouldPrintInfoLog) return;
+ if (logItem.level == QMUILogLevelWarn && !ShouldPrintWarnLog) return;
// call super
void (*originSelectorIMP)(id, SEL, const char *, int, const char *, QMUILogItem *);
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h
index 6ed6b2fd..95faf9a8 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIMarqueeLabel.h
@@ -65,3 +65,29 @@
*/
- (BOOL)requestToStopAnimation;
@end
+
+
+@interface UILabel (QMUI_Marquee)
+
+/**
+ 是否开启系统自带的跑马灯效果(系统的只能控制开启/关闭,无法控制速度、停顿等,更多功能可以使用 @c QMUIMarqueeLabel ,但论性能还是系统的更优。
+
+ 用法:
+ [label qmui_startNativeMarquee];
+ [label qmui_stopNativeMarquee]; // 当你需要停止动画时,调用这个方法(如果业务只关心什么时候开启,不关心什么时候结束,则从头到尾都可以不用调用这个方法)
+
+ @note 当开启该属性时,会强制把 numberOfLines 设置为1,clipsToBounds 设置为 YES。如果你是在 reuse view 内使用(例如 UITableViewCell/UICollectionViewCell),需要手动在 will display 时 start,did end display 时 stop。
+ */
+- (void)qmui_startNativeMarquee;
+
+/**
+ 停止跑马灯效果,与 @c qmui_startNativeMarquee 不需要成对出现,也即如果业务不关心什么时候停止动画,可以从头到尾都不调用这个方法。
+ */
+- (void)qmui_stopNativeMarquee;
+
+/**
+ 系统的跑马灯效果是否正在运行,默认为 NO。
+ */
+@property(nonatomic, assign, readonly) BOOL qmui_nativeMarqueeRunning;
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m
index c66b09da..c377270e 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIMarqueeLabel.m
@@ -232,6 +232,7 @@ - (void)checkIfShouldShowGradientLayer {
self.fadeLayer.endPoint = CGPointMake(1, .5);
self.fadeLayer.colors = @[(id)UIColorMakeWithRGBA(255, 255, 255, 0).CGColor, (id)UIColorMakeWithRGBA(255, 255, 255, 1).CGColor, (id)UIColorMakeWithRGBA(255, 255, 255, 1).CGColor, (id)UIColorMakeWithRGBA(255, 255, 255, 0).CGColor];
self.layer.mask = self.fadeLayer;
+ [self setNeedsLayout];// fadeLayer 作为 layer.mask,它依赖于在 layoutSubviews 里正确布局,否则会因为错误的 size 而导致 label 看不见
} else {
if (self.layer.mask == self.fadeLayer) {
self.layer.mask = nil;
@@ -265,3 +266,49 @@ - (BOOL)requestToStopAnimation {
}
@end
+
+@implementation UILabel (QMUI_Marquee)
+
+- (void)qmui_startNativeMarquee {
+ // 系统有 _startMarqueeIfNecessary、_startMarquee,但直接开启的方法其实是 marqueeRunning
+ BOOL running = YES;
+ self.numberOfLines = 1;
+ self.clipsToBounds = YES;
+ [self qmui_performSelector:NSSelectorFromString(@"setMarqueeEnabled:") withArguments:&running, nil];
+ [self qmui_performSelector:NSSelectorFromString(@"setMarqueeRunning:") withArguments:&running, nil];
+ [self qmuimq_removeObserver];
+ [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(qmuimq_handleApplicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
+ [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(qmuimq_handleApplicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
+}
+
+- (void)qmui_stopNativeMarquee {
+ // 系统有 _stopMarqueeWithRedisplay:,但直接关闭的方法其实是 marqueeRunning
+ BOOL running = NO;
+ [self qmui_performSelector:NSSelectorFromString(@"setMarqueeRunning:") withArguments:&running, nil];
+ [self qmui_performSelector:NSSelectorFromString(@"setMarqueeEnabled:") withArguments:&running, nil];
+ [self qmuimq_removeObserver];
+}
+
+- (BOOL)qmui_nativeMarqueeRunning {
+ BOOL running = NO;
+ [self qmui_performSelector:NSSelectorFromString(@"marqueeRunning") withPrimitiveReturnValue:&running];
+ return running;
+}
+
+- (void)qmuimq_handleApplicationDidEnterBackground:(NSNotification *)notification {
+ [self qmui_bindBOOL:self.qmui_nativeMarqueeRunning forKey:@"QMUI_Marquee_Running"];
+}
+
+- (void)qmuimq_handleApplicationDidBecomeActive:(NSNotification *)notification {
+ if ([self qmui_getBoundBOOLForKey:@"QMUI_Marquee_Running"]) {
+ [self qmui_stopNativeMarquee];// 要手动停止一次才能重新 start
+ [self qmui_startNativeMarquee];
+ }
+}
+
+- (void)qmuimq_removeObserver {
+ [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
+ [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h
index e8c5f8f7..b44011fa 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIModalPresentationViewController.h
@@ -46,9 +46,9 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) {
@optional
/**
- * 是否应该隐藏浮层,会在调用`hideWithAnimated:completion:`时,以及点击背景遮罩时被调用。默认为YES。
- * @param controller 当前的modalController
- * @return 是否允许隐藏,YES表示允许隐藏,NO表示不允许隐藏
+ * 是否应该隐藏浮层,默认为YES,会在代码主动调用隐藏,或点击背景遮罩时询问。
+ * @param controller 当前的 modalController
+ * @return 是否允许隐藏,YES 表示允许隐藏,NO 表示不允许隐藏
*/
- (BOOL)shouldHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller;
@@ -137,6 +137,24 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) {
*/
@property(nonatomic, assign) CGFloat maximumContentViewWidth UI_APPEARANCE_SELECTOR;
+/**
+ 如果 modal 是以 window 形式显示的话,通过这个属性可以获取内部实际在用的 window 对象。
+ */
+@property(nullable, nonatomic, strong, readonly) UIWindow *window;
+
+/**
+ 如果 modal 是以 window 形式显示的话,通过这个属性来决定 window 是否需要以 keyWindow 形式存在(keyWindow 一般用于与键盘交互的场景,没输入框可以不用开启它)
+ 默认为 YES。
+ */
+@property(nonatomic, assign) BOOL shouldBecomeKeyWindow;
+
+/**
+ 如果 modal 是以 window 形式显示的话,控制在 modal 显示时是否要自动把 App 主界面置灰。
+ 默认为 YES。
+ 该属性在非 window 形式显示的情况下无意义。
+ */
+@property(nonatomic, assign) BOOL shouldDimmedAppAutomatically;
+
/**
* 背景遮罩,默认为一个普通的`UIView`,背景色为`UIColorMask`,可设置为自己的view,注意`dimmingView`的大小将会盖满整个控件。
*
@@ -145,7 +163,12 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) {
@property(nullable, nonatomic, strong) IBOutlet UIView *dimmingView;
/**
- * 由于点击遮罩导致浮层被隐藏时的回调(区分于`hideWithAnimated:completion:`里的completion,这里是特地用于点击遮罩的情况)
+ * 由于点击遮罩导致浮层即将被隐藏的回调
+ */
+@property(nullable, nonatomic, copy) void (^willHideByDimmingViewTappedBlock)(void);
+
+/**
+ * 由于点击遮罩导致浮层被隐藏后的回调(区分于`hideWithAnimated:completion:`里的completion,这里是特地用于点击遮罩的情况)
*/
@property(nullable, nonatomic, copy) void (^didHideByDimmingViewTappedBlock)(void);
@@ -186,7 +209,7 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) {
@property(nonatomic, assign) BOOL onlyRespondsToKeyboardEventFromDescendantViews;
/**
- * 管理自定义的浮层布局,将会在浮层显示前、控件的容器大小发生变化时(例如横竖屏、来电状态栏)被调用
+ * 管理自定义的浮层布局,将会在浮层显示前、控件的容器大小发生变化时(例如横竖屏、来电状态栏)被调用,请在 block 内主动为 view 设置期望的 frame,设置时建议用 qmui_frameApplyTransform 取代 setFrame:,否则在有键盘的情况下,显隐动画可能有错。
* @arg containerBounds 浮层所在的父容器的大小,也即`self.view.bounds`
* @arg keyboardHeight 键盘在当前界面里的高度,若无键盘,则为0
* @arg contentViewDefaultFrame 不使用自定义布局的情况下的默认布局,会受`contentViewMargins`、`maximumContentViewWidth`、`contentView sizeThatFits:`的影响
@@ -291,7 +314,7 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) {
@end
-/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`[[UIApplication sharedApplication] windows]`里方便地区分出来
+/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`UIApplication.sharedApplication.windows`里方便地区分出来
@interface QMUIModalPresentationWindow : UIWindow
@end
@@ -300,7 +323,7 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) {
@interface UIViewController (QMUIModalPresentationViewController)
/**
- * 获取弹出当前 vieController 的 QMUIModalPresentationViewController,注意需要 modalPresentationViewController show 之后这个属性才会被赋值。
+ * 获取弹出当前 vieController 的 QMUIModalPresentationViewController
*/
@property(nullable, nonatomic, weak, readonly) QMUIModalPresentationViewController *qmui_modalPresentationViewController;
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m
index f252bca3..5355412f 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIModalPresentationViewController.m
@@ -19,6 +19,7 @@
#import "UIView+QMUI.h"
#import "QMUIKeyboardManager.h"
#import "UIWindow+QMUI.h"
+#import "QMUIAppearance.h"
@interface UIViewController ()
@@ -27,36 +28,29 @@ @interface UIViewController ()
@implementation QMUIModalPresentationViewController (UIAppearance)
-static QMUIModalPresentationViewController *appearance;
-+ (nonnull instancetype)appearance {
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
+}
+
++ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- [self initDefaultAppearance];
+ [self initAppearance];
});
- return appearance;
}
-+ (void)initDefaultAppearance {
- if (!appearance) {
- appearance = [[self alloc] init];
- }
++ (void)initAppearance {
+ QMUIModalPresentationViewController *appearance = QMUIModalPresentationViewController.appearance;
appearance.animationStyle = QMUIModalPresentationAnimationStyleFade;
appearance.contentViewMargins = UIEdgeInsetsMake(20, 20, 20, 20);
appearance.maximumContentViewWidth = CGFLOAT_MAX;
}
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self appearance];
- });
-}
-
@end
@interface QMUIModalPresentationViewController ()
-@property(nonatomic, strong) QMUIModalPresentationWindow *containerWindow;
+@property(nonatomic, strong, readwrite) QMUIModalPresentationWindow *window;
@property(nonatomic, weak) UIWindow *previousKeyWindow;
@property(nonatomic, assign, readwrite, getter=isVisible) BOOL visible;
@@ -70,12 +64,17 @@ @interface QMUIModalPresentationViewController ()
/// 标志 modal 本身以 present 的形式显示之后,又再继续 present 了一个子界面后从子界面回来时触发的 viewWillAppear:
@property(nonatomic, assign) BOOL viewWillAppearByPresentedViewController;
-/// 标志是否已经走过一次viewWillAppear了,用于hideInView的情况
+/// 标志是否已经走过一次viewWillDisappear了,用于hideInView的情况
@property(nonatomic, assign) BOOL hasAlreadyViewWillDisappear;
+/// 如果用 showInView 的方式显示浮层,则在浮层所在的父界面被 pop(或 push 到下一个界面)时,会自动触发 viewWillDisappear:,导致浮层被隐藏,为了保证走到 viewWillDisappear: 一定是手动调用 hide 的,就加了这个标志位
+/// https://github.com/Tencent/QMUI_iOS/issues/639
+@property(nonatomic, assign) BOOL willHideInView;
+
@property(nonatomic, strong) UITapGestureRecognizer *dimmingViewTapGestureRecognizer;
@property(nonatomic, strong) QMUIKeyboardManager *keyboardManager;
@property(nonatomic, assign) CGFloat keyboardHeight;
+@property(nonatomic, assign) BOOL avoidKeyboardLayout;
@end
@implementation QMUIModalPresentationViewController
@@ -95,27 +94,24 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {
}
- (void)didInitialize {
- if (appearance) {
- self.animationStyle = appearance.animationStyle;
- self.contentViewMargins = appearance.contentViewMargins;
- self.maximumContentViewWidth = appearance.maximumContentViewWidth;
- self.onlyRespondsToKeyboardEventFromDescendantViews = YES;
- self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
- self.modalPresentationStyle = UIModalPresentationCustom;
-
- // 这一段是给以 present 方式显示的浮层用的,其他方式显示的浮层,会在 supportedInterfaceOrientations 里实时获取支持的设备方向
- UIViewController *visibleViewController = [QMUIHelper visibleViewController];
- if (visibleViewController) {
- self.supportedOrientationMask = visibleViewController.supportedInterfaceOrientations;
- } else {
- self.supportedOrientationMask = SupportedOrientationMask;
- }
-
- if (self != appearance) {
- self.keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self];
- [self initDefaultDimmingViewWithoutAddToView];
- }
+ [self qmui_applyAppearance];
+
+ self.shouldDimmedAppAutomatically = YES;
+ self.onlyRespondsToKeyboardEventFromDescendantViews = YES;
+ self.shouldBecomeKeyWindow = YES;
+ self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
+ self.modalPresentationStyle = UIModalPresentationCustom;
+
+ // 这一段是给以 present 方式显示的浮层用的,其他方式显示的浮层,会在 supportedInterfaceOrientations 里实时获取支持的设备方向
+ UIViewController *visibleViewController = [QMUIHelper visibleViewController];
+ if (visibleViewController) {
+ self.supportedOrientationMask = visibleViewController.supportedInterfaceOrientations;
+ } else {
+ self.supportedOrientationMask = SupportedOrientationMask;
}
+
+ self.keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self];
+ [self initDefaultDimmingViewWithoutAddToView];
}
- (void)awakeFromNib {
@@ -127,7 +123,7 @@ - (void)awakeFromNib {
}
- (void)dealloc {
- self.containerWindow = nil;
+ self.window = nil;
}
- (BOOL)shouldAutomaticallyForwardAppearanceMethods {
@@ -158,15 +154,14 @@ - (void)viewDidLayoutSubviews {
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
+ self.visible = YES;// present 模式没有入口 show 方法,只能加在这里
+
if (self.shownInWindowMode) {
// 只有使用showWithAnimated:completion:显示出来的浮层,才需要修改之前就记住的animated的值
animated = self.appearAnimated;
}
- self.keyboardManager.delegateEnabled = YES;
-
if (self.contentViewController) {
- self.contentViewController.qmui_modalPresentationViewController = self;
[self.contentViewController beginAppearanceTransition:YES animated:animated];
}
@@ -176,17 +171,11 @@ - (void)viewWillAppear:(BOOL)animated {
return;
}
- if (self.isShownInWindowMode) {
- [QMUIHelper dimmedApplicationWindow];
- }
-
void (^didShownCompletion)(BOOL finished) = ^(BOOL finished) {
if (self.contentViewController) {
[self.contentViewController endAppearanceTransition];
}
- self.visible = YES;
-
if (self.appearCompletionBlock) {
self.appearCompletionBlock(finished);
self.appearCompletionBlock = nil;
@@ -207,6 +196,12 @@ - (void)viewWillAppear:(BOOL)animated {
contentViewFrame = self.contentView.frame;
}
self.showingAnimation(self.dimmingView, self.view.bounds, self.keyboardHeight, contentViewFrame, didShownCompletion);
+
+ if (self.shouldDimmedAppAutomatically) {
+ [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
+ [QMUIHelper dimmedApplicationWindow];
+ } completion:nil];
+ }
} else {
self.contentView.frame = contentViewFrame;
[self.contentView setNeedsLayout];
@@ -215,10 +210,12 @@ - (void)viewWillAppear:(BOOL)animated {
[self showingAnimationWithCompletion:didShownCompletion];
}
} else {
+ if (self.shouldDimmedAppAutomatically) {
+ [QMUIHelper dimmedApplicationWindow];
+ }
CGRect contentViewFrame = [self contentViewFrameForShowing];
self.contentView.frame = contentViewFrame;
[self.view addSubview:self.contentView];
- self.dimmingView.alpha = 1;
didShownCompletion(YES);
}
}
@@ -237,6 +234,12 @@ - (void)viewWillDisappear:(BOOL)animated {
return;
}
+ /// 如果用 showInView 的方式显示浮层,则在浮层所在的父界面被 pop(或 push 到下一个界面)时,会自动触发 viewWillDisappear:,导致浮层被隐藏,为了保证走到 viewWillDisappear: 一定是手动调用 hide 的,就用 willHideInView 来区分。
+ /// https://github.com/Tencent/QMUI_iOS/issues/639
+ if (self.shownInSubviewMode && !self.willHideInView) {
+ return;
+ }
+
[super viewWillDisappear:animated];
if (self.shownInWindowMode) {
@@ -251,8 +254,8 @@ - (void)viewWillDisappear:(BOOL)animated {
}
}
- // 在降下键盘前取消对键盘事件的监听,从而避免键盘影响隐藏浮层的动画
- self.keyboardManager.delegateEnabled = NO;
+ // 先更新标志位再 endEditing,保证键盘降下时不触发 updateLayout,从而避免影响 hidingAnimation 的动画
+ self.avoidKeyboardLayout = YES;
[self.view endEditing:YES];
if (self.contentViewController) {
@@ -264,32 +267,34 @@ - (void)viewWillDisappear:(BOOL)animated {
return;
}
- if (self.isShownInWindowMode) {
- [QMUIHelper resetDimmedApplicationWindow];
- }
-
void (^didHiddenCompletion)(BOOL finished) = ^(BOOL finished) {
if (self.shownInWindowMode) {
// 恢复 keyWindow 之前做一下检查,避免这个问题 https://github.com/Tencent/QMUI_iOS/issues/90
- if ([[UIApplication sharedApplication] keyWindow] == self.containerWindow) {
+ if (UIApplication.sharedApplication.keyWindow == self.window) {
if (self.previousKeyWindow.hidden) {
// 保护了这个 issue 记录的情况,避免主 window 丢失 keyWindow https://github.com/Tencent/QMUI_iOS/issues/315
- [[UIApplication sharedApplication].delegate.window makeKeyWindow];
+ [UIApplication.sharedApplication.delegate.window makeKeyWindow];
} else {
[self.previousKeyWindow makeKeyWindow];
}
}
- self.containerWindow.hidden = YES;
- self.containerWindow.rootViewController = nil;
+ self.window.hidden = YES;
+ self.window.rootViewController = nil;
self.previousKeyWindow = nil;
[self endAppearanceTransition];
}
if (self.shownInSubviewMode) {
- // 这句是给addSubview的形式显示的情况下使用,但会触发第二次viewWillDisappear:,所以要搭配self.hasAlreadyViewWillDisappear使用
+ self.willHideInView = NO;
+
[self.view removeFromSuperview];
- self.hasAlreadyViewWillDisappear = NO;
+
+ // removeFromSuperview 在 animated:YES 时会触发第二次viewWillDisappear:,所以要搭配self.hasAlreadyViewWillDisappear使用
+ // animated:NO 不会触发
+ if (animated) {
+ self.hasAlreadyViewWillDisappear = NO;
+ }
}
[self.contentView removeFromSuperview];
@@ -298,6 +303,7 @@ - (void)viewWillDisappear:(BOOL)animated {
}
self.visible = NO;
+ self.avoidKeyboardLayout = NO;
if ([self.delegate respondsToSelector:@selector(didHideModalPresentationViewController:)]) {
[self.delegate didHideModalPresentationViewController:self];
@@ -308,21 +314,24 @@ - (void)viewWillDisappear:(BOOL)animated {
self.disappearCompletionBlock = nil;
}
- if (self.contentViewController) {
- self.contentViewController.qmui_modalPresentationViewController = nil;
- self.contentViewController = nil;
- }
-
self.disappearAnimated = NO;
};
if (animated) {
if (self.hidingAnimation) {
self.hidingAnimation(self.dimmingView, self.view.bounds, self.keyboardHeight, didHiddenCompletion);
+ if (self.shouldDimmedAppAutomatically) {
+ [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveIn animations:^{
+ [QMUIHelper resetDimmedApplicationWindow];
+ } completion:nil];
+ }
} else {
[self hidingAnimationWithCompletion:didHiddenCompletion];
}
} else {
+ if (self.shouldDimmedAppAutomatically) {
+ [QMUIHelper resetDimmedApplicationWindow];
+ }
didHiddenCompletion(YES);
}
}
@@ -344,6 +353,10 @@ - (void)updateLayout {
}
}
+- (BOOL)shouldDimmedAppAutomatically {
+ return _shouldDimmedAppAutomatically && self.isShownInWindowMode;
+}
+
#pragma mark - Dimming View
- (void)setDimmingView:(UIView *)dimmingView {
@@ -397,8 +410,17 @@ - (void)handleDimmingViewTapGestureRecognizer:(UITapGestureRecognizer *)tapGestu
if (weakSelf.didHideByDimmingViewTappedBlock) {
weakSelf.didHideByDimmingViewTappedBlock();
}
- }];
+ } sender:tapGestureRecognizer];
} else if (self.shownInPresentedMode) {
+ // 这里仅屏蔽点击遮罩时的 dismiss,如果是代码手动调用 dismiss 的,在 UIViewController(QMUIModalPresentationViewController) 里会通过重写 dismiss 方法来屏蔽。
+ // 为什么不能统一交给 UIViewController(QMUIModalPresentationViewController) 里屏蔽,是因为点击遮罩触发的 dismiss 要调用 willHideByDimmingViewTappedBlock,而 UIViewController 那边不知道此次 dismiss 是否由点击遮罩触发的,所以分开两边写。
+ if ([self.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)] && ![self.delegate shouldHideModalPresentationViewController:self]) {
+ return;
+ }
+ if (self.willHideByDimmingViewTappedBlock) {
+ self.willHideByDimmingViewTappedBlock();
+ }
+
[self dismissViewControllerAnimated:YES completion:^{
if (self.didHideByDimmingViewTappedBlock) {
self.didHideByDimmingViewTappedBlock();
@@ -410,13 +432,17 @@ - (void)handleDimmingViewTapGestureRecognizer:(UITapGestureRecognizer *)tapGestu
if (weakSelf.didHideByDimmingViewTappedBlock) {
weakSelf.didHideByDimmingViewTappedBlock();
}
- }];
+ } sender:tapGestureRecognizer];
}
}
#pragma mark - ContentView
- (void)setContentViewController:(UIViewController *)contentViewController {
+ if (![contentViewController isEqual:_contentViewController]) {
+ _contentViewController.qmui_modalPresentationViewController = nil;
+ }
+ contentViewController.qmui_modalPresentationViewController = self;
_contentViewController = contentViewController;
self.contentView = contentViewController.view;
}
@@ -427,9 +453,12 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion {
if (self.animationStyle == QMUIModalPresentationAnimationStyleFade) {
self.dimmingView.alpha = 0.0;
self.contentView.alpha = 0.0;
- [UIView animateWithDuration:.2 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
+ [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
self.dimmingView.alpha = 1.0;
self.contentView.alpha = 1.0;
+ if (self.shouldDimmedAppAutomatically) {
+ [QMUIHelper dimmedApplicationWindow];
+ }
} completion:^(BOOL finished) {
if (completion) {
completion(finished);
@@ -439,9 +468,12 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion {
} else if (self.animationStyle == QMUIModalPresentationAnimationStylePopup) {
self.dimmingView.alpha = 0.0;
self.contentView.transform = CGAffineTransformMakeScale(0, 0);
- [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
+ [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
self.dimmingView.alpha = 1.0;
- self.contentView.transform = CGAffineTransformMakeScale(1, 1);
+ self.contentView.transform = CGAffineTransformIdentity;
+ if (self.shouldDimmedAppAutomatically) {
+ [QMUIHelper dimmedApplicationWindow];
+ }
} completion:^(BOOL finished) {
self.contentView.transform = CGAffineTransformIdentity;
if (completion) {
@@ -452,9 +484,12 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion {
} else if (self.animationStyle == QMUIModalPresentationAnimationStyleSlide) {
self.dimmingView.alpha = 0.0;
self.contentView.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(self.view.bounds) - CGRectGetMinY(self.contentView.frame));
- [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
+ [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
self.dimmingView.alpha = 1.0;
self.contentView.transform = CGAffineTransformIdentity;
+ if (self.shouldDimmedAppAutomatically) {
+ [QMUIHelper dimmedApplicationWindow];
+ }
} completion:^(BOOL finished) {
if (completion) {
completion(finished);
@@ -464,46 +499,66 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion {
}
- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
+ if (self.visible) return;
+ self.visible = YES;
+
// makeKeyAndVisible 导致的 viewWillAppear: 必定 animated 是 NO 的,所以这里用额外的变量保存这个 animated 的值
self.appearAnimated = animated;
self.appearCompletionBlock = completion;
- self.previousKeyWindow = [UIApplication sharedApplication].keyWindow;
- if (!self.containerWindow) {
- self.containerWindow = [[QMUIModalPresentationWindow alloc] init];
- self.containerWindow.qmui_capturesStatusBarAppearance = NO;// modalPrensetationViewController.contentViewController 默认无权管理状态栏的样式,如需修改状态栏,请业务自己将这个属性改为 YES
- self.containerWindow.windowLevel = UIWindowLevelQMUIAlertView;
- self.containerWindow.backgroundColor = UIColorClear;// 避免横竖屏旋转时出现黑色
+ self.previousKeyWindow = UIApplication.sharedApplication.keyWindow;
+ if (!self.window) {
+ self.window = [[QMUIModalPresentationWindow alloc] init];
+ self.window.windowLevel = UIWindowLevelQMUIAlertView;
+ self.window.backgroundColor = UIColorClear;// 避免横竖屏旋转时出现黑色
+ [self updateWindowStatusBarCapture];
+ }
+ self.window.rootViewController = self;
+ if (self.shouldBecomeKeyWindow) {
+ [self.window makeKeyAndVisible];
+ } else {
+ self.window.hidden = NO;
}
- self.containerWindow.rootViewController = self;
- [self.containerWindow makeKeyAndVisible];
}
- (void)hidingAnimationWithCompletion:(void (^)(BOOL))completion {
if (self.animationStyle == QMUIModalPresentationAnimationStyleFade) {
- [UIView animateWithDuration:.2 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
+ [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
self.dimmingView.alpha = 0.0;
self.contentView.alpha = 0.0;
+ if (self.shouldDimmedAppAutomatically) {
+ [QMUIHelper resetDimmedApplicationWindow];
+ }
} completion:^(BOOL finished) {
if (completion) {
+ self.dimmingView.alpha = 1.0;
+ self.contentView.alpha = 1.0;
completion(finished);
}
}];
} else if (self.animationStyle == QMUIModalPresentationAnimationStylePopup) {
- [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
+ [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
self.dimmingView.alpha = 0.0;
- self.contentView.transform = CGAffineTransformMakeScale(0.0, 0.0);
+ self.contentView.transform = CGAffineTransformMakeScale(0.01, 0.01);
+ if (self.shouldDimmedAppAutomatically) {
+ [QMUIHelper resetDimmedApplicationWindow];
+ }
} completion:^(BOOL finished) {
if (completion) {
+ self.dimmingView.alpha = 1.0;
self.contentView.transform = CGAffineTransformIdentity;
completion(finished);
}
}];
} else if (self.animationStyle == QMUIModalPresentationAnimationStyleSlide) {
- [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
+ [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{
self.dimmingView.alpha = 0.0;
self.contentView.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(self.view.bounds) - CGRectGetMinY(self.contentView.frame));
+ if (self.shouldDimmedAppAutomatically) {
+ [QMUIHelper resetDimmedApplicationWindow];
+ }
} completion:^(BOOL finished) {
if (completion) {
+ self.dimmingView.alpha = 1.0;
self.contentView.transform = CGAffineTransformIdentity;
completion(finished);
}
@@ -512,6 +567,12 @@ - (void)hidingAnimationWithCompletion:(void (^)(BOOL))completion {
}
- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
+ [self hideWithAnimated:animated completion:completion sender:nil];
+}
+
+- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion sender:(id)sender {
+ if (!self.visible) return;
+
self.disappearAnimated = animated;
self.disappearCompletionBlock = completion;
@@ -523,6 +584,12 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
return;
}
+ if (sender == self.dimmingViewTapGestureRecognizer) {
+ if (self.willHideByDimmingViewTappedBlock) {
+ self.willHideByDimmingViewTappedBlock();
+ }
+ }
+
// window模式下,通过手动触发viewWillDisappear:来做界面消失的逻辑
if (self.shownInWindowMode) {
[self beginAppearanceTransition:NO animated:animated];
@@ -530,6 +597,9 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
}
- (void)showInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL))completion {
+ if (self.visible) return;
+ self.visible = YES;
+
self.appearCompletionBlock = completion;
[self loadViewIfNeeded];
[self beginAppearanceTransition:YES animated:animated];
@@ -538,9 +608,33 @@ - (void)showInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(B
}
- (void)hideInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL))completion {
+ [self hideInView:view animated:animated completion:completion sender:nil];
+}
+
+- (void)hideInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL))completion sender:(id)sender {
+ if (!self.visible) return;
+
+ BOOL shouldHide = YES;
+ if ([self.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)]) {
+ shouldHide = [self.delegate shouldHideModalPresentationViewController:self];
+ }
+ if (!shouldHide) {
+ return;
+ }
+
+ self.willHideInView = YES;
+
+ if (sender == self.dimmingViewTapGestureRecognizer) {
+ if (self.willHideByDimmingViewTappedBlock) {
+ self.willHideByDimmingViewTappedBlock();
+ }
+ }
+
self.disappearCompletionBlock = completion;
[self beginAppearanceTransition:NO animated:animated];
- self.hasAlreadyViewWillDisappear = YES;
+ if (animated) {
+ self.hasAlreadyViewWillDisappear = YES;
+ }
[self endAppearanceTransition];
}
@@ -560,7 +654,7 @@ - (CGRect)contentViewFrameForShowing {
}
- (BOOL)isShownInWindowMode {
- return !!self.containerWindow;
+ return !!self.window;
}
- (BOOL)isShownInPresentedMode {
@@ -568,7 +662,7 @@ - (BOOL)isShownInPresentedMode {
}
- (BOOL)isShownInSubviewMode {
- return !self.shownInPresentedMode && self.view.superview;
+ return !self.shownInWindowMode && !self.shownInPresentedMode && self.view.superview;
}
- (BOOL)isShowingPresentedViewController {
@@ -584,8 +678,13 @@ - (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUser
return;
}
}
- self.keyboardHeight = [keyboardUserInfo heightInView:self.view];
- [self updateLayout];
+ CGFloat keyboardHeight = [keyboardUserInfo heightInView:self.view];
+ if (self.keyboardHeight != keyboardHeight) {
+ self.keyboardHeight = keyboardHeight;
+ if (!self.avoidKeyboardLayout) {
+ [self updateLayout];
+ }
+ }
}
#pragma mark - 屏幕旋转
@@ -606,20 +705,40 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return self.supportedOrientationMask;
}
-- (UIViewController *)childViewControllerForStatusBarStyle {
- if (self.shownInPresentedMode) {
- return self.contentViewController;
+- (void)setQmui_prefersStatusBarHiddenBlock:(BOOL (^)(void))qmui_prefersStatusBarHiddenBlock {
+ [super setQmui_prefersStatusBarHiddenBlock:qmui_prefersStatusBarHiddenBlock];
+ [self updateWindowStatusBarCapture];
+}
+
+- (void)setQmui_preferredStatusBarStyleBlock:(UIStatusBarStyle (^)(void))qmui_preferredStatusBarStyleBlock {
+ [super setQmui_preferredStatusBarStyleBlock:qmui_preferredStatusBarStyleBlock];
+ [self updateWindowStatusBarCapture];
+}
+
+- (void)updateWindowStatusBarCapture {
+ if (!self.window) return;
+ // 当以 window 的方式显示浮层时,状态栏交给 QMUIModalPresentationViewController 控制
+ self.window.qmui_capturesStatusBarAppearance = self.qmui_prefersStatusBarHiddenBlock || self.qmui_preferredStatusBarStyleBlock;
+ if (self.window.qmui_capturesStatusBarAppearance) {
+ [self setNeedsStatusBarAppearanceUpdate];
}
- return [super childViewControllerForStatusBarStyle];
}
+// 当以 present 方式显示浮层时,状态栏允许由 contentViewController 控制,但 QMUIModalPresentationViewController 的 qmui_prefersStatusBarHiddenBlock/qmui_preferredStatusBarStyleBlock 优先级会更高
- (UIViewController *)childViewControllerForStatusBarHidden {
- if (self.shownInPresentedMode) {
+ if (self.shownInPresentedMode && self.contentViewController) {
return self.contentViewController;
}
return [super childViewControllerForStatusBarHidden];
}
+- (UIViewController *)childViewControllerForStatusBarStyle {
+ if (self.shownInPresentedMode && self.contentViewController) {
+ return self.contentViewController;
+ }
+ return [super childViewControllerForStatusBarStyle];
+}
+
- (UIViewController *)childViewControllerForHomeIndicatorAutoHidden {
if (self.shownInPresentedMode) {
return self.contentViewController;
@@ -632,7 +751,7 @@ - (UIViewController *)childViewControllerForHomeIndicatorAutoHidden {
@implementation QMUIModalPresentationViewController (Manager)
+ (BOOL)isAnyModalPresentationViewControllerVisible {
- for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
+ for (UIWindow *window in UIApplication.sharedApplication.windows) {
if ([window isKindOfClass:[QMUIModalPresentationWindow class]] && !window.hidden) {
return YES;
}
@@ -644,7 +763,7 @@ + (BOOL)hideAllVisibleModalPresentationViewControllerIfCan {
BOOL hideAllFinally = YES;
- for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
+ for (UIWindow *window in UIApplication.sharedApplication.windows) {
if (![window isKindOfClass:[QMUIModalPresentationWindow class]]) {
continue;
}
@@ -691,21 +810,40 @@ + (BOOL)hideAllVisibleModalPresentationViewControllerIfCan {
@implementation QMUIModalPresentationWindow
-- (void)layoutSubviews {
- [super layoutSubviews];
- if (self.rootViewController) {
- // https://github.com/Tencent/QMUI_iOS/issues/375
- UIView *rootView = self.rootViewController.view;
- if (CGRectGetMinY(rootView.frame) > 0 && ![UIApplication sharedApplication].statusBarHidden && StatusBarHeight > CGRectGetMinY(rootView.frame)) {
- rootView.frame = self.bounds;
- }
- }
-}
-
@end
@implementation UIViewController (QMUIModalPresentationViewController)
QMUISynthesizeIdWeakProperty(qmui_modalPresentationViewController, setQmui_modalPresentationViewController)
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+
+ // present 方式显示的 modal,通过拦截 dismiss 方法来实现 shouldHide 的 delegate。注意以 window 方式显示的 modal,在 window.rootViewController = nil 时系统默认也会调用 dismiss,此时要通过 isShownInPresentedMode 区分开。
+ OverrideImplementation([UIViewController class], @selector(dismissViewControllerAnimated:completion:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UIViewController *selfObject, BOOL firstArgv, id secondArgv) {
+
+ QMUIModalPresentationViewController *modal = nil;
+ if ([selfObject.presentedViewController isKindOfClass:QMUIModalPresentationViewController.class]) {
+ modal = (QMUIModalPresentationViewController *)selfObject.presentedViewController;
+ } else if ([selfObject isKindOfClass:QMUIModalPresentationViewController.class] && !selfObject.presentedViewController && selfObject.presentingViewController.presentedViewController == selfObject) {
+ modal = (QMUIModalPresentationViewController *)selfObject;
+ }
+ if ([modal.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)] && modal.isShownInPresentedMode) {
+ BOOL shouldHide = [modal.delegate shouldHideModalPresentationViewController:modal];
+ if (!shouldHide) {
+ return;
+ }
+ }
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, BOOL, id);
+ originSelectorIMP = (void (*)(id, SEL, BOOL, id))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv);
+ };
+ });
+ });
+}
+
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h
index b76f4490..f84addaa 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIMoreOperationController.h
@@ -53,7 +53,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface QMUIMoreOperationController : UIViewController
@property(nullable, nonatomic, strong) UIColor *contentBackgroundColor UI_APPEARANCE_SELECTOR;// 面板上半部分(不包含取消按钮)背景色
-@property(nonatomic, assign) CGFloat contentEdgeMargin UI_APPEARANCE_SELECTOR;// 面板距离屏幕的上下左右间距
+@property(nonatomic, assign) UIEdgeInsets contentEdgeMargins UI_APPEARANCE_SELECTOR;// 面板距离屏幕的上下左右间距
@property(nonatomic, assign) CGFloat contentMaximumWidth UI_APPEARANCE_SELECTOR;// 面板的最大宽度
@property(nonatomic, assign) CGFloat contentCornerRadius UI_APPEARANCE_SELECTOR;// 面板的圆角大小,当值大于 0 时会设置 self.view.clipsToBounds = YES
@property(nonatomic, assign) UIEdgeInsets contentPaddings UI_APPEARANCE_SELECTOR;// 面板内部的 padding,UIScrollView 会布局在除去 padding 之后的区域
@@ -79,7 +79,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(nullable, nonatomic, weak) id delegate;
@property(nullable, nonatomic, strong, readonly) UIView *contentView;// 放 UIScrollView 的容器,与 cancelButton 区分开
-@property(nullable, nonatomic, strong, readonly) NSArray *scrollViews;// 获取当前的所有 UIScrollView
+@property(nullable, nonatomic, copy, readonly) NSArray *scrollViews;// 获取当前的所有 UIScrollView
/// 取消按钮,如果不需要,则自行设置其 hidden 为 YES
@property(nullable, nonatomic, strong, readonly) QMUIButton *cancelButton;
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m
index 2a1ff042..269b7a46 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIMoreOperationController.m
@@ -21,6 +21,7 @@
#import "NSArray+QMUI.h"
#import "UIScrollView+QMUI.h"
#import "QMUILog.h"
+#import "QMUIAppearance.h"
static NSInteger const kQMUIMoreOperationItemViewTagOffset = 999;
@@ -38,51 +39,44 @@ - (void)formatItemViewStyleWithMoreOperationController:(QMUIMoreOperationControl
@implementation QMUIMoreOperationController (UIAppearance)
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self appearance];
- });
++ (instancetype)appearance {
+ return [QMUIAppearance appearanceForClass:self];
}
-static QMUIMoreOperationController *moreOperationViewControllerAppearance;
-+ (nonnull instancetype)appearance {
++ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
- [self resetAppearance];
+ [self initAppearance];
});
- return moreOperationViewControllerAppearance;
}
-+ (void)resetAppearance {
- if (!moreOperationViewControllerAppearance) {
- moreOperationViewControllerAppearance = [[QMUIMoreOperationController alloc] init];
- moreOperationViewControllerAppearance.contentBackgroundColor = UIColorWhite;
- moreOperationViewControllerAppearance.contentEdgeMargin = 10;
- moreOperationViewControllerAppearance.contentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - moreOperationViewControllerAppearance.contentEdgeMargin * 2;
- moreOperationViewControllerAppearance.contentCornerRadius = 10;
- moreOperationViewControllerAppearance.contentPaddings = UIEdgeInsetsMake(10, 0, 5, 0);
-
- moreOperationViewControllerAppearance.scrollViewSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f);
- moreOperationViewControllerAppearance.scrollViewContentInsets = UIEdgeInsetsMake(14, 8, 14, 8);
-
- moreOperationViewControllerAppearance.itemBackgroundColor = UIColorClear;
- moreOperationViewControllerAppearance.itemTitleColor = UIColorGrayDarken;
- moreOperationViewControllerAppearance.itemTitleFont = UIFontMake(11);
- moreOperationViewControllerAppearance.itemPaddingHorizontal = 16;
- moreOperationViewControllerAppearance.itemTitleMarginTop = 9;
- moreOperationViewControllerAppearance.itemMinimumMarginHorizontal = 0;
- moreOperationViewControllerAppearance.automaticallyAdjustItemMargins = YES;
-
- moreOperationViewControllerAppearance.cancelButtonBackgroundColor = UIColorWhite;
- moreOperationViewControllerAppearance.cancelButtonTitleColor = UIColorBlue;
- moreOperationViewControllerAppearance.cancelButtonSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f);
- moreOperationViewControllerAppearance.cancelButtonFont = UIFontBoldMake(16);
- moreOperationViewControllerAppearance.cancelButtonHeight = 56.0;
- moreOperationViewControllerAppearance.cancelButtonMarginTop = 0;
-
- moreOperationViewControllerAppearance.isExtendBottomLayout = NO;
- }
++ (void)initAppearance {
+ QMUIMoreOperationController *moreOperationViewControllerAppearance = QMUIMoreOperationController.appearance;
+ moreOperationViewControllerAppearance.contentBackgroundColor = UIColorForBackground;
+ moreOperationViewControllerAppearance.contentEdgeMargins = UIEdgeInsetsMake(0, 10, 10, 10);
+ moreOperationViewControllerAppearance.contentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(moreOperationViewControllerAppearance.contentEdgeMargins);
+ moreOperationViewControllerAppearance.contentCornerRadius = 10;
+ moreOperationViewControllerAppearance.contentPaddings = UIEdgeInsetsMake(10, 0, 5, 0);
+
+ moreOperationViewControllerAppearance.scrollViewSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f);
+ moreOperationViewControllerAppearance.scrollViewContentInsets = UIEdgeInsetsMake(14, 8, 14, 8);
+
+ moreOperationViewControllerAppearance.itemBackgroundColor = UIColorClear;
+ moreOperationViewControllerAppearance.itemTitleColor = UIColorGrayDarken;
+ moreOperationViewControllerAppearance.itemTitleFont = UIFontMake(11);
+ moreOperationViewControllerAppearance.itemPaddingHorizontal = 16;
+ moreOperationViewControllerAppearance.itemTitleMarginTop = 9;
+ moreOperationViewControllerAppearance.itemMinimumMarginHorizontal = 0;
+ moreOperationViewControllerAppearance.automaticallyAdjustItemMargins = YES;
+
+ moreOperationViewControllerAppearance.cancelButtonBackgroundColor = UIColorForBackground;
+ moreOperationViewControllerAppearance.cancelButtonTitleColor = UIColorBlue;
+ moreOperationViewControllerAppearance.cancelButtonSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f);
+ moreOperationViewControllerAppearance.cancelButtonFont = UIFontBoldMake(16);
+ moreOperationViewControllerAppearance.cancelButtonHeight = 56.0;
+ moreOperationViewControllerAppearance.cancelButtonMarginTop = 0;
+
+ moreOperationViewControllerAppearance.isExtendBottomLayout = NO;
}
@end
@@ -116,58 +110,45 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder {
}
- (void)didInitialize {
- if (moreOperationViewControllerAppearance) {
- self.contentBackgroundColor = [QMUIMoreOperationController appearance].contentBackgroundColor;
- self.contentEdgeMargin = [QMUIMoreOperationController appearance].contentEdgeMargin;
- self.contentMaximumWidth = [QMUIMoreOperationController appearance].contentMaximumWidth;
- self.contentCornerRadius = [QMUIMoreOperationController appearance].contentCornerRadius;
- self.contentPaddings = [QMUIMoreOperationController appearance].contentPaddings;
-
- self.scrollViewSeparatorColor = [QMUIMoreOperationController appearance].scrollViewSeparatorColor;
- self.scrollViewContentInsets = [QMUIMoreOperationController appearance].scrollViewContentInsets;
-
- self.itemBackgroundColor = [QMUIMoreOperationController appearance].itemBackgroundColor;
- self.itemTitleColor = [QMUIMoreOperationController appearance].itemTitleColor;
- self.itemTitleFont = [QMUIMoreOperationController appearance].itemTitleFont;
- self.itemPaddingHorizontal = [QMUIMoreOperationController appearance].itemPaddingHorizontal;
- self.itemTitleMarginTop = [QMUIMoreOperationController appearance].itemTitleMarginTop;
- self.itemMinimumMarginHorizontal = [QMUIMoreOperationController appearance].itemMinimumMarginHorizontal;
- self.automaticallyAdjustItemMargins = [QMUIMoreOperationController appearance].automaticallyAdjustItemMargins;
-
- self.cancelButtonBackgroundColor = [QMUIMoreOperationController appearance].cancelButtonBackgroundColor;
- self.cancelButtonTitleColor = [QMUIMoreOperationController appearance].cancelButtonTitleColor;
- self.cancelButtonSeparatorColor = [QMUIMoreOperationController appearance].cancelButtonSeparatorColor;
- self.cancelButtonFont = [QMUIMoreOperationController appearance].cancelButtonFont;
- self.cancelButtonHeight = [QMUIMoreOperationController appearance].cancelButtonHeight;
- self.cancelButtonMarginTop = [QMUIMoreOperationController appearance].cancelButtonMarginTop;
-
- self.isExtendBottomLayout = [QMUIMoreOperationController appearance].isExtendBottomLayout;
-
- self.mutableScrollViews = [[NSMutableArray alloc] init];
- self.mutableItems = [[NSMutableArray alloc] init];
- }
+ [self qmui_applyAppearance];
- [self loadViewIfNeeded];
+ self.mutableScrollViews = [[NSMutableArray alloc] init];
+ self.mutableItems = [[NSMutableArray alloc] init];
+}
+
+#pragma mark - Getters & Setters
+
+@synthesize contentView = _contentView;
+- (UIView *)contentView {
+ if (!_contentView) {
+ _contentView = [[UIView alloc] init];
+ _contentView.backgroundColor = self.contentBackgroundColor;
+ }
+ return _contentView;
+}
+
+@synthesize cancelButton = _cancelButton;
+- (QMUIButton *)cancelButton {
+ if (!_cancelButton) {
+ _cancelButton = [[QMUIButton alloc] init];
+ _cancelButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES;
+ _cancelButton.adjustsButtonWhenHighlighted = NO;
+ _cancelButton.titleLabel.font = self.cancelButtonFont;
+ _cancelButton.backgroundColor = self.cancelButtonBackgroundColor;
+ [_cancelButton setTitle:@"取消" forState:UIControlStateNormal];
+ [_cancelButton setTitleColor:self.cancelButtonTitleColor forState:UIControlStateNormal];
+ [_cancelButton setTitleColor:[self.cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted];
+ _cancelButton.qmui_borderPosition = self.cancelButtonMarginTop > 0 ? QMUIViewBorderPositionNone : QMUIViewBorderPositionTop;
+ _cancelButton.qmui_borderColor = self.cancelButtonSeparatorColor;
+ [_cancelButton addTarget:self action:@selector(handleCancelButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
+ }
+ return _cancelButton;
}
- (void)viewDidLoad {
[super viewDidLoad];
- _contentView = [[UIView alloc] init];
- self.contentView.backgroundColor = self.contentBackgroundColor;
[self.view addSubview:self.contentView];
-
- _cancelButton = [[QMUIButton alloc] init];
- self.cancelButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES;
- self.cancelButton.adjustsButtonWhenHighlighted = NO;
- self.cancelButton.titleLabel.font = self.cancelButtonFont;
- self.cancelButton.backgroundColor = self.cancelButtonBackgroundColor;
- [self.cancelButton setTitle:@"取消" forState:UIControlStateNormal];
- [self.cancelButton setTitleColor:self.cancelButtonTitleColor forState:UIControlStateNormal];
- [self.cancelButton setTitleColor:[self.cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted];
- self.cancelButton.qmui_borderPosition = QMUIViewBorderPositionBottom;
- self.cancelButton.qmui_borderColor = self.cancelButtonSeparatorColor;
- [self.cancelButton addTarget:self action:@selector(handleCancelButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.cancelButton];
self.extendLayer = [CALayer layer];
@@ -211,10 +192,10 @@ - (void)viewDidLayoutSubviews {
[self.mutableScrollViews enumerateObjectsUsingBlock:^(UIScrollView * _Nonnull scrollView, NSUInteger idx, BOOL * _Nonnull stop) {
scrollView.frame = CGRectMake(self.contentPaddings.left, layoutY, contentWidth, CGRectGetHeight(scrollView.frame));
- // 要保护 safeAreaInsets 的区域,而这里不使用 scrollView.qmui_safeAreaInsets 是因为此时 scrollView 的 safeAreaInsets 仍然为 0,但 scrollView.superview.safeAreaInsets 已经正确了,所以使用 scrollView.superview 也即 self.view 的
+ // 要保护 safeAreaInsets 的区域,而这里不使用 scrollView.safeAreaInsets 是因为此时 scrollView 的 safeAreaInsets 仍然为 0,但 scrollView.superview.safeAreaInsets 已经正确了,所以使用 scrollView.superview 也即 self.view 的
// 底部的 insets 暂不考虑
-// UIEdgeInsets scrollViewSafeAreaInsets = scrollView.qmui_safeAreaInsets;
- UIEdgeInsets scrollViewSafeAreaInsets = UIEdgeInsetsMake(fmax(self.view.qmui_safeAreaInsets.top - scrollView.qmui_top, 0), fmax(self.view.qmui_safeAreaInsets.left - scrollView.qmui_left, 0), 0, fmax(self.view.qmui_safeAreaInsets.right - (self.view.qmui_width - scrollView.qmui_right), 0));
+// UIEdgeInsets scrollViewSafeAreaInsets = scrollView.safeAreaInsets;
+ UIEdgeInsets scrollViewSafeAreaInsets = UIEdgeInsetsMake(fmax(self.view.safeAreaInsets.top - scrollView.qmui_top, 0), fmax(self.view.safeAreaInsets.left - scrollView.qmui_left, 0), 0, fmax(self.view.safeAreaInsets.right - (self.view.qmui_width - scrollView.qmui_right), 0));
NSArray *itemSection = self.mutableItems[idx];
QMUIMoreOperationItemView *exampleItemView = itemSection.firstObject;
@@ -262,12 +243,12 @@ - (void)showFromBottom {
QMUIModalPresentationViewController *modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init];
modalPresentationViewController.delegate = self;
modalPresentationViewController.maximumContentViewWidth = self.contentMaximumWidth;
- modalPresentationViewController.contentViewMargins = UIEdgeInsetsMake(self.contentEdgeMargin, self.contentEdgeMargin, self.contentEdgeMargin, self.contentEdgeMargin);
+ modalPresentationViewController.contentViewMargins = self.contentEdgeMargins;
modalPresentationViewController.contentViewController = self;
__weak __typeof(modalPresentationViewController)weakModalController = modalPresentationViewController;
modalPresentationViewController.layoutBlock = ^(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame) {
- weakModalController.contentView.frame = CGRectSetY(contentViewDefaultFrame, CGRectGetHeight(containerBounds) - weakModalController.contentViewMargins.bottom - CGRectGetHeight(contentViewDefaultFrame) - weakModalController.view.qmui_safeAreaInsets.bottom);
+ weakModalController.contentView.qmui_frameApplyTransform = CGRectSetY(contentViewDefaultFrame, CGRectGetHeight(containerBounds) - weakModalController.contentViewMargins.bottom - CGRectGetHeight(contentViewDefaultFrame) - weakModalController.view.safeAreaInsets.bottom);
};
modalPresentationViewController.showingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void(^completion)(BOOL finished)) {
@@ -440,9 +421,7 @@ - (UIScrollView *)generateScrollViewWithIndex:(NSInteger)index {
scrollView.qmui_borderColor = self.scrollViewSeparatorColor;
scrollView.qmui_borderPosition = (self.scrollViewSeparatorColor && index != 0) ? QMUIViewBorderPositionTop : QMUIViewBorderPositionNone;
scrollView.scrollsToTop = NO;
- if (@available(iOS 11, *)) {
- scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
- }
+ scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
scrollView.contentInset = self.scrollViewContentInsets;
[scrollView qmui_scrollToTopForce:YES animated:NO];
return scrollView;
@@ -477,7 +456,7 @@ - (void)handleItemViewEvent:(QMUIMoreOperationItemView *)itemView {
- (void)setContentBackgroundColor:(UIColor *)contentBackgroundColor {
_contentBackgroundColor = contentBackgroundColor;
- self.contentView.backgroundColor = contentBackgroundColor;
+ _contentView.backgroundColor = contentBackgroundColor;
}
- (void)setScrollViewSeparatorColor:(UIColor *)scrollViewSeparatorColor {
@@ -497,21 +476,21 @@ - (void)setScrollViewContentInsets:(UIEdgeInsets)scrollViewContentInsets {
- (void)setCancelButtonBackgroundColor:(UIColor *)cancelButtonBackgroundColor {
_cancelButtonBackgroundColor = cancelButtonBackgroundColor;
- self.cancelButton.backgroundColor = cancelButtonBackgroundColor;
+ _cancelButton.backgroundColor = cancelButtonBackgroundColor;
[self updateExtendLayerAppearance];
}
- (void)setCancelButtonTitleColor:(UIColor *)cancelButtonTitleColor {
_cancelButtonTitleColor = cancelButtonTitleColor;
- if (self.cancelButton) {
- [self.cancelButton setTitleColor:cancelButtonTitleColor forState:UIControlStateNormal];
- [self.cancelButton setTitleColor:[cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted];
+ if (_cancelButton) {
+ [_cancelButton setTitleColor:cancelButtonTitleColor forState:UIControlStateNormal];
+ [_cancelButton setTitleColor:[cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted];
}
}
- (void)setCancelButtonSeparatorColor:(UIColor *)cancelButtonSeparatorColor {
_cancelButtonSeparatorColor = cancelButtonSeparatorColor;
- self.cancelButton.qmui_borderColor = cancelButtonSeparatorColor;
+ _cancelButton.qmui_borderColor = cancelButtonSeparatorColor;
}
- (void)setItemBackgroundColor:(UIColor *)itemBackgroundColor {
@@ -561,8 +540,8 @@ - (void)setAutomaticallyAdjustItemMargins:(BOOL)automaticallyAdjustItemMargins {
- (void)setCancelButtonFont:(UIFont *)cancelButtonFont {
_cancelButtonFont = cancelButtonFont;
- self.cancelButton.titleLabel.font = cancelButtonFont;
- [self.cancelButton setNeedsLayout];
+ _cancelButton.titleLabel.font = cancelButtonFont;
+ [_cancelButton setNeedsLayout];
}
- (void)setContentCornerRadius:(CGFloat)contentCornerRadius {
@@ -572,7 +551,7 @@ - (void)setContentCornerRadius:(CGFloat)contentCornerRadius {
- (void)setCancelButtonMarginTop:(CGFloat)cancelButtonMarginTop {
_cancelButtonMarginTop = cancelButtonMarginTop;
- self.cancelButton.qmui_borderPosition = cancelButtonMarginTop > 0 ? QMUIViewBorderPositionNone : QMUIViewBorderPositionTop;
+ _cancelButton.qmui_borderPosition = cancelButtonMarginTop > 0 ? QMUIViewBorderPositionNone : QMUIViewBorderPositionTop;
[self updateCornerRadius];
[self setViewNeedsLayoutIfLoaded];
}
@@ -602,16 +581,20 @@ - (void)updateExtendLayerAppearance {
- (void)updateCornerRadius {
if (self.cancelButtonMarginTop > 0) {
- self.view.layer.cornerRadius = 0;
- self.view.clipsToBounds = NO;
+ if (self.isViewLoaded) {
+ self.view.layer.cornerRadius = 0;
+ self.view.clipsToBounds = NO;
+ }
- self.contentView.layer.cornerRadius = self.contentCornerRadius;
- self.cancelButton.layer.cornerRadius = self.contentCornerRadius;
+ _contentView.layer.cornerRadius = self.contentCornerRadius;
+ _cancelButton.layer.cornerRadius = self.contentCornerRadius;
} else {
- self.view.layer.cornerRadius = self.contentCornerRadius;
- self.view.clipsToBounds = self.view.layer.cornerRadius > 0;// 有圆角才需要 clip
- self.contentView.layer.cornerRadius = 0;
- self.cancelButton.layer.cornerRadius = 0;
+ if (self.isViewLoaded) {
+ self.view.layer.cornerRadius = self.contentCornerRadius;
+ self.view.clipsToBounds = self.view.layer.cornerRadius > 0;// 有圆角才需要 clip
+ }
+ _contentView.layer.cornerRadius = 0;
+ _cancelButton.layer.cornerRadius = 0;
}
}
@@ -667,7 +650,7 @@ @implementation QMUIMoreOperationItemView
@dynamic tag;
+ (instancetype)itemViewWithImage:(UIImage *)image selectedImage:(UIImage *)selectedImage title:(NSString *)title selectedTitle:(NSString *)selectedTitle handler:(void (^)(QMUIMoreOperationController *, QMUIMoreOperationItemView *))handler {
- QMUIMoreOperationItemView *itemView = [[QMUIMoreOperationItemView alloc] init];
+ QMUIMoreOperationItemView *itemView = [[self alloc] init];
[itemView setImage:image forState:UIControlStateNormal];
[itemView setImage:selectedImage forState:UIControlStateSelected];
[itemView setImage:selectedImage forState:UIControlStateHighlighted|UIControlStateSelected];
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h
index 58e760d3..e891a641 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// NSObject+MultipleDelegates.h
@@ -27,6 +27,8 @@
* [self qmui_registerDelegateSelector:@selector(abcDelegate)];
* self.abcDelegate = delegateA;
* self.abcDelegate = delegateB;
+ *
+ * @warning 不支持 self.delegate = self 的写法,会引发死循环,有这种需求的场景建议在 self 内部创建一个对象专门用于 delegate 的响应,参考 _QMUITextViewDelegator。
*/
@interface NSObject (QMUIMultipleDelegates)
@@ -40,10 +42,3 @@
- (void)qmui_removeDelegate:(id)delegate;
@end
-
-@interface NSObject (QMUIMultipleDelegates_Private)
-
-/// 内部使用,用于标志“xxx.delegate = xxx”的情况,具体请看 https://github.com/Tencent/QMUI_iOS/issues/346
-@property(nonatomic, assign) BOOL qmui_delegatesSelf;
-
-@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m
index f18af983..9b4b9531 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// NSObject+MultipleDelegates.m
@@ -17,7 +17,7 @@
#import "QMUIMultipleDelegates.h"
#import "QMUICore.h"
#import "NSPointerArray+QMUI.h"
-#import
+#import "NSString+QMUI.h"
@interface NSObject ()
@@ -28,8 +28,6 @@ @implementation NSObject (QMUIMultipleDelegates)
QMUISynthesizeIdStrongProperty(qmuimd_delegates, setQmuimd_delegates)
-static NSMutableSet *qmui_methodsReplacedClasses;
-
static char kAssociatedObjectKey_qmuiMultipleDelegatesEnabled;
- (void)setQmui_multipleDelegatesEnabled:(BOOL)qmui_multipleDelegatesEnabled {
objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiMultipleDelegatesEnabled, @(qmui_multipleDelegatesEnabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
@@ -54,39 +52,16 @@ - (void)qmui_registerDelegateSelector:(SEL)getter {
}
Class targetClass = [self class];
- SEL originDelegateSetter = [self originSetterWithGetter:getter];
+ SEL originDelegateSetter = setterWithGetter(getter);
SEL newDelegateSetter = [self newSetterWithGetter:getter];
Method originMethod = class_getInstanceMethod(targetClass, originDelegateSetter);
if (!originMethod) {
return;
}
- // 为这个 selector 创建一个 QMUIMultipleDelegates 容器
NSString *delegateGetterKey = NSStringFromSelector(getter);
- if (!self.qmuimd_delegates[delegateGetterKey]) {
- objc_property_t prop = class_getProperty(self.class, delegateGetterKey.UTF8String);
- QMUIPropertyDescriptor *property = [QMUIPropertyDescriptor descriptorWithProperty:prop];
- if (property.isStrong) {
- // strong property
- QMUIMultipleDelegates *strongDelegates = [QMUIMultipleDelegates strongDelegates];
- strongDelegates.parentObject = self;
- self.qmuimd_delegates[delegateGetterKey] = strongDelegates;
- } else {
- // weak property
- QMUIMultipleDelegates *weakDelegates = [QMUIMultipleDelegates weakDelegates];
- weakDelegates.parentObject = self;
- self.qmuimd_delegates[delegateGetterKey] = weakDelegates;
- }
- }
- // 避免为某个 class 重复替换同一个方法的实现
- if (!qmui_methodsReplacedClasses) {
- qmui_methodsReplacedClasses = [NSMutableSet set];
- }
- NSString *classAndMethodIdentifier = [NSString stringWithFormat:@"%@-%@", NSStringFromClass(targetClass), delegateGetterKey];
- if (![qmui_methodsReplacedClasses containsObject:classAndMethodIdentifier]) {
- [qmui_methodsReplacedClasses addObject:classAndMethodIdentifier];
-
+ [QMUIHelper executeBlock:^{
IMP originIMP = method_getImplementation(originMethod);
void (*originSelectorIMP)(id, SEL, id);
originSelectorIMP = (void (*)(id, SEL, id))originIMP;
@@ -99,24 +74,38 @@ - (void)qmui_registerDelegateSelector:(SEL)getter {
return;
}
+ // 为这个 selector 创建一个 QMUIMultipleDelegates 容器
QMUIMultipleDelegates *delegates = selfObject.qmuimd_delegates[delegateGetterKey];
-
if (!aDelegate) {
// 对应 setDelegate:nil,表示清理所有的 delegate
- [delegates removeAllDelegates];
- selfObject.qmui_delegatesSelf = NO;
- // 只要 qmui_multipleDelegatesEnabled 开启,就会保证 delegate 一直是 delegates,所以不去调用系统默认的 set nil
- // originSelectorIMP(selfObject, originDelegateSetter, nil);
+ if (delegates) {
+ [delegates removeAllDelegates];
+ [selfObject.qmuimd_delegates removeObjectForKey:delegateGetterKey];
+ }
+ // 必须要清空,否则遇到像 tableView:cellForRowAtIndexPath: 这种“要求返回值不能为 nil” 的场景就会中 assert
+ // https://github.com/Tencent/QMUI_iOS/issues/1411
+ originSelectorIMP(selfObject, originDelegateSetter, nil);
return;
}
+ if (!delegates) {
+ objc_property_t prop = class_getProperty(selfObject.class, delegateGetterKey.UTF8String);
+ QMUIPropertyDescriptor *property = [QMUIPropertyDescriptor descriptorWithProperty:prop];
+ if (property.isStrong) {
+ // strong property
+ delegates = [QMUIMultipleDelegates strongDelegates];
+ } else {
+ // weak property
+ delegates = [QMUIMultipleDelegates weakDelegates];
+ }
+ delegates.parentObject = selfObject;
+ selfObject.qmuimd_delegates[delegateGetterKey] = delegates;
+ }
+
if (aDelegate != delegates) {// 过滤掉容器自身,避免把 delegates 传进去 delegates 里,导致死循环
[delegates addDelegate:aDelegate];
}
- // 将类似 textView.delegate = textView 的情况标志起来,避免产生循环调用 https://github.com/Tencent/QMUI_iOS/issues/346
- selfObject.qmui_delegatesSelf = [delegates.delegates qmui_containsPointer:(__bridge void * _Nullable)(selfObject)];
-
originSelectorIMP(selfObject, originDelegateSetter, nil);// 先置为 nil 再设置 delegates,从而避免这个问题 https://github.com/Tencent/QMUI_iOS/issues/305
originSelectorIMP(selfObject, originDelegateSetter, delegates);// 不管外面将什么 object 传给 setDelegate:,最终实际上传进去的都是 QMUIMultipleDelegates 容器
@@ -125,7 +114,7 @@ - (void)qmui_registerDelegateSelector:(SEL)getter {
Method newMethod = class_getInstanceMethod(targetClass, newDelegateSetter);
method_exchangeImplementations(originMethod, newMethod);
}
- }
+ } oncePerIdentifier:[NSString stringWithFormat:@"MultipleDelegates %@-%@", NSStringFromClass(targetClass), NSStringFromSelector(getter)]];
// 如果原来已经有 delegate,则将其加到新建的容器里
// @see https://github.com/Tencent/QMUI_iOS/issues/378
@@ -138,7 +127,7 @@ - (void)qmui_registerDelegateSelector:(SEL)getter {
}
- (void)qmui_removeDelegate:(id)delegate {
- if (!self.qmui_multipleDelegatesEnabled) {
+ if (!self.qmuimd_delegates) {
return;
}
NSMutableArray *delegateGetters = [[NSMutableArray alloc] init];
@@ -164,26 +153,9 @@ - (void)refreshDelegateWithGetter:(SEL)getter {
EndIgnorePerformSelectorLeaksWarning
}
-// 根据 delegate property 的 getter,得到它对应的 setter
-- (SEL)originSetterWithGetter:(SEL)getter {
- NSString *getterString = NSStringFromSelector(getter);
- NSMutableString *setterString = [[NSMutableString alloc] initWithString:@"set"];
- [setterString appendString:[getterString substringToIndex:1].uppercaseString];
- [setterString appendString:[getterString substringFromIndex:1]];
- [setterString appendString:@":"];
- SEL setter = NSSelectorFromString(setterString);
- return setter;
-}
-
// 根据 delegate property 的 getter,得到 QMUIMultipleDelegates 为它的 setter 创建的新 setter 方法,最终交换原方法,因此利用这个方法返回的 SEL,可以调用到原来的 delegate property setter 的实现
- (SEL)newSetterWithGetter:(SEL)getter {
- return NSSelectorFromString([NSString stringWithFormat:@"qmuimd_%@", NSStringFromSelector([self originSetterWithGetter:getter])]);
+ return NSSelectorFromString([NSString stringWithFormat:@"qmuimd_%@", NSStringFromSelector(setterWithGetter(getter))]);
}
@end
-
-@implementation NSObject (QMUIMultipleDelegates_Private)
-
-QMUISynthesizeBOOLProperty(qmui_delegatesSelf, setQmui_delegatesSelf)
-
-@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h
index a8c2b130..c6c22146 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIMultipleDelegates.h
@@ -30,4 +30,5 @@
- (BOOL)removeDelegate:(id)delegate;
- (void)removeAllDelegates;
- (BOOL)containsDelegate:(id)delegate;
+
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m
index 74e7bceb..d825d4fd 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIMultipleDelegates.m
@@ -15,30 +15,53 @@
#import "QMUIMultipleDelegates.h"
#import "NSPointerArray+QMUI.h"
-#import
+#import "NSMethodSignature+QMUI.h"
+#import "NSObject+QMUI.h"
+#import "QMUICore.h"
@interface QMUIMultipleDelegates ()
@property(nonatomic, strong, readwrite) NSPointerArray *delegates;
+@property(nonatomic, strong) NSInvocation *forwardingInvocation;
+@property(nonatomic, assign) SEL inquiringSelector;
@end
@implementation QMUIMultipleDelegates
+ (instancetype)weakDelegates {
- QMUIMultipleDelegates *delegates = [[QMUIMultipleDelegates alloc] init];
+ QMUIMultipleDelegates *delegates = [[self alloc] init];
delegates.delegates = [NSPointerArray weakObjectsPointerArray];
return delegates;
}
+ (instancetype)strongDelegates {
- QMUIMultipleDelegates *delegates = [[QMUIMultipleDelegates alloc] init];
+ QMUIMultipleDelegates *delegates = [[self alloc] init];
delegates.delegates = [NSPointerArray strongObjectsPointerArray];
return delegates;
}
+- (void)resetClassNameIfNeeded {
+ if ([self.parentObject isKindOfClass:CALayer.class] || [self.parentObject isKindOfClass:CAAnimation.class]) {
+ // CALayer 和 CAAnimation 会缓存同一个 delegate class 的 respondsToSelector: 结果,但是在 multipleDelegates 的设计下,可能存在当前的 delegate 无法响应某个 selector,而后添加了可以响应的 delegate,系统这个缓存机制仍会认为无法响应,所以每次添加新的 delegate 都要设置与之前不同的 className
+ // 这里设置一个 QMUIMultipleDelegates 的 subClass,其 className 由所有 delegate className 拼接而成。
+ NSMutableString *className = [NSMutableString stringWithString:NSStringFromClass(QMUIMultipleDelegates.class)];
+ [self.delegates.allObjects enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
+ NSString *delegateClassName = NSStringFromClass(object_getClass(delegate));
+ [className appendFormat:@"_%@", delegateClassName];
+ }];
+ Class class = NSClassFromString(className);
+ if (!class) {
+ class = objc_allocateClassPair(QMUIMultipleDelegates.class, className.UTF8String, 0);
+ objc_registerClassPair(class);
+ }
+ object_setClass(self, class);
+ }
+}
+
- (void)addDelegate:(id)delegate {
if (![self containsDelegate:delegate] && delegate != self) {
[self.delegates addPointer:(__bridge void *)delegate];
+ [self resetClassNameIfNeeded];
}
}
@@ -62,11 +85,7 @@ - (BOOL)containsDelegate:(id)delegate {
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
- NSMethodSignature *result = [super methodSignatureForSelector:aSelector];
- if (result) {
- return result;
- }
-
+ NSMethodSignature *result = nil;
NSPointerArray *delegates = [self.delegates copy];
for (id delegate in delegates) {
result = [delegate methodSignatureForSelector:aSelector];
@@ -74,28 +93,53 @@ - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return result;
}
}
- // https://github.com/facebookarchive/AsyncDisplayKit/pull/1562
- // Unfortunately, in order to get this object to work properly, the use of a method which creates an NSMethodSignature
- // from a C string. -methodSignatureForSelector is called when a compiled definition for the selector cannot be found.
- // This is the place where we have to create our own dud NSMethodSignature. This is necessary because if this method
- // returns nil, a selector not found exception is raised. The string argument to -signatureWithObjCTypes: outlines
- // the return type and arguments to the message. To return a dud NSMethodSignature, pretty much any signature will
- // suffice. Since the -forwardInvocation call will do nothing if the delegate does not respond to the selector,
- // the dud NSMethodSignature simply gets us around the exception.
- return [NSMethodSignature signatureWithObjCTypes:"@^v^c"];
+
+ return NSMethodSignature.qmui_avoidExceptionSignature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = anInvocation.selector;
- NSPointerArray *delegates = [self.delegates copy];
+
+ // RAC 那边会把相同的 invocation 传回来 QMUIMultipleDelegates,引发死循环,所以这里做了个屏蔽
+ // https://github.com/Tencent/QMUI_iOS/issues/970
+ if (self.forwardingInvocation.selector != NULL && self.forwardingInvocation.selector == selector) {
+ NSUInteger returnLength = anInvocation.methodSignature.methodReturnLength;
+ if (returnLength) {
+ void *buffer = (void *)malloc(returnLength);
+ [self.forwardingInvocation getReturnValue:buffer];
+ [anInvocation setReturnValue:buffer];
+ free(buffer);
+ }
+ return;
+ }
+
+ NSPointerArray *delegates = self.delegates.copy;
for (id delegate in delegates) {
if ([delegate respondsToSelector:selector]) {
+ // 当前 delegate 的实现可能再次调用原始 delegate 的实现,如果原始 delegate 是 QMUIMultipleDelegates 就会造成死循环,所以要做 2 事:
+ // 1、检测到循环就打破
+ // 2、但是检测到循环时,新生成的 anInvocation 默认没有 returnValue,需要用上一次循环之前的结果
+ self.forwardingInvocation = anInvocation;
[anInvocation invokeWithTarget:delegate];
}
}
+
+ self.forwardingInvocation = nil;
}
- (BOOL)respondsToSelector:(SEL)aSelector {
+
+ if (self.inquiringSelector == aSelector) {
+ /**
+ 这个判断是为了避免类似 RACDelegateProxy 的处理导致的死循环:
+ RACDelegateProxy 会做以下事情:
+ 1.保存之前的代理
+ 2.把对象代理修改为 RACDelegateProxy
+ 由于 QMUIMultipleDelegates 会拦截操作 2,保持原始代理一直是 QMUIMultipleDelegates 不被修改,同时把 RACDelegateProxy 添加到 delegates,而 RACDelegateProxy 操作 1 又保存了 QMUIMultipleDelegates 实例,当对其调用 respondsToSelector 时,又会转发到 QMUIMultipleDelegates 造成死循环,所以要做这个保护。
+ */
+ return NO;
+ }
+
if ([super respondsToSelector:aSelector]) {
return YES;
}
@@ -107,19 +151,84 @@ - (BOOL)respondsToSelector:(SEL)aSelector {
}
// 对 QMUIMultipleDelegates 额外处理的解释在这里:https://github.com/Tencent/QMUI_iOS/issues/357
- BOOL delegateCanRespondToSelector = [delegate isKindOfClass:self.class] ? [delegate respondsToSelector:aSelector] : class_respondsToSelector(((NSObject *)delegate).class, aSelector);
-
- // 判断 qmui_delegatesSelf 是为了解决这个 issue:https://github.com/Tencent/QMUI_iOS/issues/346
- BOOL isDelegateSelf = ((NSObject *)delegate).qmui_delegatesSelf;
- if (delegateCanRespondToSelector && !isDelegateSelf) {
+ BOOL delegateCanRespondToSelector;
+ if ([delegate isProxy] || [delegate isKindOfClass:QMUIMultipleDelegates.class]) {
+ self.inquiringSelector = aSelector;
+ delegateCanRespondToSelector = [delegate respondsToSelector:aSelector];
+ self.inquiringSelector = NULL;
+ } else {
+ delegateCanRespondToSelector = class_respondsToSelector(object_getClass(delegate), aSelector);
+ }
+ if (delegateCanRespondToSelector) {
return YES;
}
}
return NO;
}
+#pragma mark - Overrides
+
+- (BOOL)isProxy {
+ return YES;
+}
+
+- (BOOL)isKindOfClass:(Class)aClass {
+ BOOL result = [super isKindOfClass:aClass];
+ if (result) return YES;
+
+ NSPointerArray *delegates = [self.delegates copy];
+ for (id delegate in delegates) {
+ if ([delegate isKindOfClass:aClass]) return YES;
+ }
+
+ return NO;
+}
+
+- (BOOL)isMemberOfClass:(Class)aClass {
+ BOOL result = [super isMemberOfClass:aClass];
+ if (result) return YES;
+
+ NSPointerArray *delegates = [self.delegates copy];
+ for (id delegate in delegates) {
+ if ([delegate isMemberOfClass:aClass]) return YES;
+ }
+
+ return NO;
+}
+
+- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
+ BOOL result = [super conformsToProtocol:aProtocol];
+ if (result) return YES;
+
+ NSPointerArray *delegates = [self.delegates copy];
+ for (id delegate in delegates) {
+ if ([delegate conformsToProtocol:aProtocol]) return YES;
+ }
+
+ return NO;
+}
+
- (NSString *)description {
return [NSString stringWithFormat:@"%@, parentObject is %@, %@", [super description], self.parentObject, self.delegates];
}
+- (id)valueForKey:(NSString *)key {
+ NSPointerArray *delegates = [self.delegates copy];
+ for (id delegate in delegates) {
+ if ([delegate qmui_canGetValueForKey:key]) {
+ return [delegate valueForKey:key];
+ }
+ }
+ return [super valueForKey:key];
+}
+
+- (void)setValue:(id)value forKey:(NSString *)key {
+ NSPointerArray *delegates = [self.delegates copy];
+ for (id delegate in delegates) {
+ if ([delegate qmui_canSetValueForKey:key]) {
+ [delegate setValue:value forKey:key];
+ }
+ }
+}
+
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h
index fae3fd9c..180f9163 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUINavigationTitleView.h
@@ -54,7 +54,9 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) {
/**
- * 可作为navgationItem.titleView 的标题控件。
+ * 可作为 UIViewController 顶部导航栏里的标题控件,通过 self.navigationItem.titleView 来设置。当调用 -[UIViewController setTitle:] 或 -[UINavigationItem setTitle:] 时,会自动更新 QMUINavigationTitleView 的内容。
+ *
+ * 也可以当成单独的组件,脱离 UIViewController 使用,就跟普通组件一样。
*
* 支持主副标题,且可控制主副标题的布局方式(水平或垂直);支持在左边显示loading,在右边显示accessoryView(如箭头)。
*
@@ -70,6 +72,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) {
@property(nonatomic, weak) id delegate;
@property(nonatomic, assign) QMUINavigationTitleViewStyle style;
@property(nonatomic, assign, getter=isActive) BOOL active;
+@property(nonatomic, assign) UIEdgeInsets padding UI_APPEARANCE_SELECTOR;
@property(nonatomic, assign) CGFloat maximumWidth UI_APPEARANCE_SELECTOR;
#pragma mark - Titles
@@ -80,6 +83,16 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) {
@property(nonatomic, strong, readonly) UILabel *subtitleLabel;
@property(nonatomic, copy) NSString *subtitle;
+/// 当 tintColor 发生变化时是否要自动把 titleLabel、subtitleLabel、loadingView 的颜色也更新为 tintColor 的色值,默认为 YES,如果你自己修改了 titleLabel、subtitleLabel、loadingView 的颜色,需要把这个值置为 NO
+@property(nonatomic, assign) BOOL adjustsSubviewsTintColorAutomatically UI_APPEARANCE_SELECTOR;
+
+/**
+ * 是否自动调整 highlighted 时的样式,默认为YES。
+ * 当值为 YES 时,标题 highlighted 时会改变自身的 alpha 属性为 UIControlHighlightedAlpha
+ * 适用于比如说整个 titleView 不需要接受点击,但 accessoryView 需要接受点击,此时就应该 titleView.userInteractionEnabled = YES、titleView.adjustsSubviewsWhenHighlighted = NO
+ */
+@property(nonatomic, assign) BOOL adjustsSubviewsWhenHighlighted;
+
/// 水平布局下的标题字体,默认为 NavBarTitleFont
@property(nonatomic, strong) UIFont *horizontalTitleFont UI_APPEARANCE_SELECTOR;
@@ -115,7 +128,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) {
@property(nonatomic, assign) BOOL loadingViewHidden;
/*
- * 如果为YES则title居中,loading放在title的左边,title右边有一个跟左边loading一样大的占位空间;如果为NO,loading和title整体居中。默认值为YES。
+ * 如果为YES则title居中,loading放在title的左边,title右边有一个跟左边loading一样大的占位空间(目的是为了让切换 loading 时文字不跳动);如果为NO,loading和title整体居中。默认值为YES。
*/
@property(nonatomic, assign) BOOL needsLoadingPlaceholderSpace;
@@ -170,3 +183,10 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) {
- (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style;
@end
+
+@interface UIView (QMUINavigationTitleView)
+
+/// 标记当前 view 是用于自定义的导航栏标题,QMUI 可以帮你自动处理系统的一些布局 bug,并且保证 pop 时导航栏标题颜色不会被前一个界面影响。
+/// 对于 QMUINavigationTitleView 而言默认值为 YES,其他 UIView 默认值为 NO。
+@property(nonatomic, assign) BOOL qmui_useAsNavigationTitleView;
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m
index 8e225969..fb4a4bcd 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUINavigationTitleView.m
@@ -19,59 +19,14 @@
#import "UIImage+QMUI.h"
#import "UILabel+QMUI.h"
#import "UIActivityIndicatorView+QMUI.h"
+#import "UIViewController+QMUI.h"
#import "UIView+QMUI.h"
-
-@interface UINavigationBar (TitleView)
-
-@end
-
-@implementation UINavigationBar (TitleView)
-
-+ (void)load {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- OverrideImplementation([UINavigationBar class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
- return ^(UINavigationBar *selfObject) {
-
- // avoid superclass
- if ([selfObject isKindOfClass:originClass]) {
- QMUINavigationTitleView *titleView = (QMUINavigationTitleView *)selfObject.topItem.titleView;
-
- if ([titleView isKindOfClass:[QMUINavigationTitleView class]]) {
- CGFloat titleViewMaximumWidth = CGRectGetWidth(titleView.bounds);// 初始状态下titleView会被设置为UINavigationBar允许的最大宽度
- CGSize titleViewSize = [titleView sizeThatFits:CGSizeMake(titleViewMaximumWidth, CGFLOAT_MAX)];
- titleViewSize.height = ceil(titleViewSize.height);// titleView的高度如果非pt整数,会导致计算出来的y值时多时少,所以干脆做一下pt取整,这个策略不要改,改了要重新测试push过程中titleView是否会跳动
-
- // 当在UINavigationBar里使用自定义的titleView时,就算titleView的sizeThatFits:返回正确的高度,navigationBar也不会帮你设置高度(但会帮你设置宽度),所以我们需要自己更新高度并且修正y值
- if (CGRectGetHeight(titleView.bounds) != titleViewSize.height) {
- CGFloat titleViewMinY = flat(CGRectGetMinY(titleView.frame) - ((titleViewSize.height - CGRectGetHeight(titleView.bounds)) / 2.0));// 系统对titleView的y值布局是flat,注意,不能改,改了要测试
- titleView.frame = CGRectMake(CGRectGetMinX(titleView.frame), titleViewMinY, MIN(titleViewMaximumWidth, titleViewSize.width), titleViewSize.height);
- }
-
- // iOS 11 之后(iOS 11 Beta 5 测试过) titleView 的布局发生了一些变化,如果不主动设置宽度,titleView 里的内容就可能无法完整展示
- if (@available(iOS 11, *)) {
- if (CGRectGetWidth(titleView.bounds) != titleViewSize.width) {
- titleView.frame = CGRectSetWidth(titleView.frame, titleViewSize.width);
- }
- }
- } else {
- titleView = nil;
- }
- }
-
- // call super
- void (*originSelectorIMP)(id, SEL);
- originSelectorIMP = (void (*)(id, SEL))originalIMPProvider();
- originSelectorIMP(selfObject, originCMD);
- };
- });
- });
-}
-
-@end
+#import "UINavigationItem+QMUI.h"
+#import "QMUIAppearance.h"
@interface QMUINavigationTitleView ()
+@property(nonatomic, strong, readonly) UIView *contentView;
@property(nonatomic, assign) CGSize titleLabelSize;
@property(nonatomic, assign) CGSize subtitleLabelSize;
@property(nonatomic, strong) UIImageView *accessoryTypeView;
@@ -93,17 +48,24 @@ - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style {
- (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style frame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
+ self.qmui_useAsNavigationTitleView = YES;
+ self.qmui_outsideEdge = UIEdgeInsetsMake(-10, 0, -10, 0);
[self addTarget:self action:@selector(handleTouchTitleViewEvent) forControlEvents:UIControlEventTouchUpInside];
+ _contentView = [[UIView alloc] init];
+ [self addSubview:self.contentView];
+
_titleLabel = [[UILabel alloc] init];
self.titleLabel.textAlignment = NSTextAlignmentCenter;
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
- [self addSubview:self.titleLabel];
+ self.titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader;
+ [self.contentView addSubview:self.titleLabel];
_subtitleLabel = [[UILabel alloc] init];
self.subtitleLabel.textAlignment = NSTextAlignmentCenter;
self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
- [self addSubview:self.subtitleLabel];
+ self.subtitleLabel.accessibilityTraits |= UIAccessibilityTraitHeader;
+ [self.contentView addSubview:self.subtitleLabel];
self.userInteractionEnabled = NO;
self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
@@ -115,17 +77,13 @@ - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style frame:(CGRect)
self.needsLoadingPlaceholderSpace = YES;
self.accessoryType = QMUINavigationTitleViewAccessoryTypeNone;
- QMUINavigationTitleView *appearance = [QMUINavigationTitleView appearance];
- self.maximumWidth = appearance.maximumWidth;
- self.loadingViewSize = appearance.loadingViewSize;
- self.loadingViewMarginRight = appearance.loadingViewMarginRight;
- self.horizontalTitleFont = appearance.horizontalTitleFont ?: [UINavigationBar appearance].titleTextAttributes[NSFontAttributeName];
- self.horizontalSubtitleFont = appearance.horizontalSubtitleFont ?: self.horizontalTitleFont;
- self.verticalTitleFont = appearance.verticalTitleFont;
- self.verticalSubtitleFont = appearance.verticalSubtitleFont;
- self.accessoryViewOffset = appearance.accessoryViewOffset;
- self.subAccessoryViewOffset = appearance.subAccessoryViewOffset;
- self.tintColor = QMUICMIActivated ? NavBarTitleColor : [UINavigationBar appearance].titleTextAttributes[NSForegroundColorAttributeName];
+ [self qmui_applyAppearance];
+ self.horizontalTitleFont = QMUINavigationTitleView.appearance.horizontalTitleFont ?: UINavigationBar.qmui_appearanceConfigured.titleTextAttributes[NSFontAttributeName];
+ self.horizontalSubtitleFont = QMUINavigationTitleView.appearance.horizontalSubtitleFont ?: self.horizontalTitleFont;
+
+ self.adjustsSubviewsTintColorAutomatically = QMUINavigationTitleView.appearance.adjustsSubviewsTintColorAutomatically;
+ self.adjustsSubviewsWhenHighlighted = QMUINavigationTitleView.appearance.adjustsSubviewsWhenHighlighted;
+ self.tintColor = QMUICMIActivated ? NavBarTitleColor : UINavigationBar.qmui_appearanceConfigured.titleTextAttributes[NSForegroundColorAttributeName];
}
return self;
}
@@ -144,6 +102,13 @@ - (void)refreshLayout {
[self setNeedsLayout];
}
+- (void)setNeedsLayout {
+ [self updateTitleLabelSize];
+ [self updateSubtitleLabelSize];
+ [self updateSubAccessoryViewHidden];
+ [super setNeedsLayout];
+}
+
// 找到 titleView 所在的 navigationBar(iOS 11 及以后,titleView.superview.superview == navigationBar,iOS 10 及以前,titleView.superview == navigationBar)
- (UINavigationBar *)navigationBarSuperviewForSubview:(UIView *)subview {
if (!subview.superview) {
@@ -176,14 +141,17 @@ - (void)updateSubtitleLabelSize {
}
- (CGSize)loadingViewSpacingSize {
- if (self.needsLoadingView) {
- return CGSizeMake(self.loadingViewSize.width + self.loadingViewMarginRight, self.loadingViewSize.height);
+ if (self.needsLoadingView && (self.needsLoadingPlaceholderSpace || !self.loadingViewHidden)) {
+ // 意味着希望保持 title 绝对居中,所以不管 loading 是否显示,都固定留空位给 loading
+ CGSize size = CGSizeMake(self.loadingViewSize.width + self.loadingViewMarginRight, self.loadingViewSize.height);
+ return size;
}
return CGSizeZero;
}
- (CGSize)loadingViewSpacingSizeIfNeedsPlaceholder {
- return CGSizeMake([self loadingViewSpacingSize].width * (self.needsLoadingPlaceholderSpace ? 2 : 1), [self loadingViewSpacingSize].height);
+ CGSize size = CGSizeMake([self loadingViewSpacingSize].width * (self.needsLoadingPlaceholderSpace ? 2 : 1), [self loadingViewSpacingSize].height);
+ return size;
}
- (CGSize)accessorySpacingSize {
@@ -197,7 +165,7 @@ - (CGSize)accessorySpacingSize {
- (CGSize)subAccessorySpacingSize {
if (self.subAccessoryView) {
UIView *view = self.subAccessoryView;
- return CGSizeMake(CGRectGetWidth(view.bounds) + self.subAccessoryViewOffset.x, CGRectGetHeight(view.bounds));
+ return CGSizeMake(CGRectGetWidth(view.frame) + self.subAccessoryViewOffset.x, CGRectGetHeight(view.frame));
}
return CGSizeZero;
}
@@ -218,23 +186,30 @@ - (UIEdgeInsets)subtitleEdgeInsetsIfShowingSubtitleLabel {
return CGSizeIsEmpty(self.subtitleLabelSize) ? UIEdgeInsetsZero : self.subtitleEdgeInsets;
}
+- (CGFloat)firstLineWidthInVerticalStyle {
+ CGFloat firstLineWidth = self.titleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsetsIfShowingTitleLabel);
+ firstLineWidth += [self loadingViewSpacingSizeIfNeedsPlaceholder].width;
+ firstLineWidth += [self accessorySpacingSizeIfNeedesPlaceholder].width;
+ return firstLineWidth;
+}
+
+- (CGFloat)secondLineWidthInVerticalStyle {
+ CGFloat secondLineWidth = self.subtitleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel);
+ if (self.subtitleLabelSize.width > 0 && self.subAccessoryView && !self.subAccessoryView.hidden) {
+ secondLineWidth += [self subAccessorySpacingSizeIfNeedesPlaceholder].width;
+ }
+ return secondLineWidth;
+}
+
- (CGSize)contentSize {
if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) {
CGSize size = CGSizeZero;
- // 垂直排列的情况下,loading和accessory与titleLabel同一行
- CGFloat firstLineWidth = self.titleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsetsIfShowingTitleLabel);
- firstLineWidth += [self loadingViewSpacingSizeIfNeedsPlaceholder].width;
- firstLineWidth += [self accessorySpacingSizeIfNeedesPlaceholder].width;
-
- CGFloat secondLineWidth = self.subtitleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel);
- if (self.subtitleLabelSize.width > 0 && self.subAccessoryView && !self.subAccessoryView.hidden) {
- secondLineWidth += [self subAccessorySpacingSizeIfNeedesPlaceholder].width;
- }
-
+ CGFloat firstLineWidth = [self firstLineWidthInVerticalStyle];// 垂直排列的情况下,loading和accessory与titleLabel同一行
+ CGFloat secondLineWidth = [self secondLineWidthInVerticalStyle];
size.width = MAX(firstLineWidth, secondLineWidth);
- size.height = self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsetsIfShowingTitleLabel) + self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel);
+ size.height = self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsetsIfShowingTitleLabel) + self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel);// 虽然 accessoryView、loadingView 的高度都可能超过文字本身高度,但为了方便,这里就只考虑文字高度,其他 subview 高度均不考虑,布局时都相对于文字垂直居中(可能会溢出 titleView 的上下边缘)
return CGSizeFlatted(size);
} else {
CGSize size = CGSizeZero;
@@ -249,7 +224,9 @@ - (CGSize)contentSize {
- (CGSize)sizeThatFits:(CGSize)size {
CGSize resultSize = [self contentSize];
+ resultSize.width += UIEdgeInsetsGetHorizontalValue(self.padding);
resultSize.width = MIN(resultSize.width, self.maximumWidth);
+ resultSize.height += UIEdgeInsetsGetVerticalValue(self.padding);
return resultSize;
}
@@ -260,29 +237,22 @@ - (void)layoutSubviews {
[super layoutSubviews];
+ self.contentView.frame = CGRectInsetEdges(self.bounds, self.padding);
+
BOOL alignLeft = self.contentHorizontalAlignment == UIControlContentHorizontalAlignmentLeft;
BOOL alignRight = self.contentHorizontalAlignment == UIControlContentHorizontalAlignmentRight;
// 通过sizeThatFit计算出来的size,如果大于可使用的最大宽度,则会被系统改为最大限制的最大宽度
- CGSize maxSize = self.bounds.size;
+ CGSize maxSize = self.contentView.bounds.size;
// 实际内容的size,小于等于maxSize
CGSize contentSize = [self contentSize];
contentSize.width = MIN(maxSize.width, contentSize.width);
contentSize.height = MIN(maxSize.height, contentSize.height);
- // 计算左右两边的偏移值
- CGFloat offsetLeft = 0;
- CGFloat offsetRight = 0;
- if (alignLeft) {
- offsetLeft = 0;
- offsetRight = maxSize.width - contentSize.width;
- } else if (alignRight) {
- offsetLeft = maxSize.width - contentSize.width;
- offsetRight = 0;
- } else {
- offsetLeft = offsetRight = floorInPixel((maxSize.width - contentSize.width) / 2.0);
- }
+ // 整个 titleView 居中,但内部的 subviews 可以在内容区域根据 contentHorizontalAlignment 的值做不一样的对齐布局
+ CGFloat contentOffsetLeft = floorInPixel((maxSize.width - contentSize.width) / 2.0);
+ CGFloat contentOffsetRight = contentOffsetLeft;
// 计算loading占的单边宽度
CGFloat loadingViewSpace = [self loadingViewSpacingSize].width;
@@ -299,59 +269,69 @@ - (void)layoutSubviews {
UIEdgeInsets titleEdgeInsets = self.titleEdgeInsetsIfShowingTitleLabel;
UIEdgeInsets subtitleEdgeInsets = self.subtitleEdgeInsetsIfShowingSubtitleLabel;
- CGFloat minX = offsetLeft + (self.needsAccessoryPlaceholderSpace ? accessoryViewSpace : 0);
- CGFloat maxX = maxSize.width - offsetRight - (self.needsLoadingPlaceholderSpace ? loadingViewSpace : 0);
-
if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) {
-
+ CGFloat firstLineWidth = [self firstLineWidthInVerticalStyle];// 这里得到的是实际内容宽度,可能会超出 contentSize.width,所以下方会用 MIN/MAX 做保护
+ CGFloat firstLineMinX = 0;
+ CGFloat firstLineMaxX = 0;
+ if (alignLeft) {
+ firstLineMinX = contentOffsetLeft;
+ } else if (alignRight) {
+ firstLineMinX = MAX(contentOffsetLeft, contentOffsetLeft + contentSize.width - firstLineWidth);
+ } else {
+ firstLineMinX = contentOffsetLeft + MAX(0, CGFloatGetCenter(contentSize.width, firstLineWidth));
+ }
+ firstLineMaxX = firstLineMinX + MIN(firstLineWidth, contentSize.width) - (self.needsLoadingPlaceholderSpace ? [self loadingViewSpacingSize].width : 0);
+ firstLineMinX += self.needsAccessoryPlaceholderSpace ? accessoryViewSpace : 0;
if (self.loadingView) {
- self.loadingView.frame = CGRectSetXY(self.loadingView.frame, minX, CGFloatGetCenter(self.titleLabelSize.height, self.loadingViewSize.height) + titleEdgeInsets.top);
- minX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight;
+ if (self.needsLoadingPlaceholderSpace || !self.loadingView.hidden) {
+ self.loadingView.frame = CGRectSetXY(self.loadingView.frame, firstLineMinX, CGFloatGetCenter(self.titleLabelSize.height, self.loadingViewSize.height) + titleEdgeInsets.top);
+ firstLineMinX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight;
+ }
}
if (accessoryView) {
- accessoryView.frame = CGRectSetXY(accessoryView.frame, maxX - CGRectGetWidth(accessoryView.bounds), CGFloatGetCenter(self.titleLabelSize.height, CGRectGetHeight(accessoryView.bounds)) + titleEdgeInsets.top + self.accessoryViewOffset.y);
- maxX = CGRectGetMinX(accessoryView.frame) - self.accessoryViewOffset.x;
+ accessoryView.frame = CGRectSetXY(accessoryView.frame, firstLineMaxX - CGRectGetWidth(accessoryView.frame), CGFloatGetCenter(self.titleLabelSize.height, CGRectGetHeight(accessoryView.frame)) + titleEdgeInsets.top + self.accessoryViewOffset.y);
+ firstLineMaxX = CGRectGetMinX(accessoryView.frame) - self.accessoryViewOffset.x;
}
if (isTitleLabelShowing) {
- minX += titleEdgeInsets.left;
- maxX -= titleEdgeInsets.right;
- self.titleLabel.frame = CGRectFlatMake(minX, titleEdgeInsets.top, maxX - minX, self.titleLabelSize.height);
+ firstLineMinX += titleEdgeInsets.left;
+ firstLineMaxX -= titleEdgeInsets.right;
+ self.titleLabel.frame = CGRectFlatMake(firstLineMinX, titleEdgeInsets.top, firstLineMaxX - firstLineMinX, self.titleLabelSize.height);
} else {
self.titleLabel.frame = CGRectZero;
}
+
if (isSubtitleLabelShowing) {
- [self.subtitleLabel sizeToFit];
- CGFloat subtitleMinX = subtitleEdgeInsets.left;
- CGFloat subtitleWidth = maxSize.width - UIEdgeInsetsGetHorizontalValue(subtitleEdgeInsets);
-
- if (isSubAccessoryViewShowing) {
- if (self.needsSubAccessoryPlaceholderSpace) {
- // 副标题居中,accessoryView 在右边
- CGFloat subtitleMaxWidth = subtitleWidth - [self subAccessorySpacingSizeIfNeedesPlaceholder].width;
- subtitleWidth = MIN(CGRectGetWidth(self.subtitleLabel.bounds), subtitleMaxWidth);
- subtitleMinX = (CGRectGetWidth(self.bounds) - subtitleWidth) / 2.0;
- } else {
- // 整体居中
- CGFloat subtitleMaxWidth = subtitleWidth - [self subAccessorySpacingSize].width;
- subtitleWidth = MIN(CGRectGetWidth(self.subtitleLabel.bounds), subtitleMaxWidth);
- subtitleMinX = (CGRectGetWidth(self.bounds) - subtitleWidth - [self subAccessorySpacingSize].width) / 2.0;
- }
+ CGFloat secondLineWidth = [self secondLineWidthInVerticalStyle];
+ CGFloat secondLineMinX = 0;
+ CGFloat secondLineMaxX = 0;
+ CGFloat secondLineMinY = subtitleEdgeInsets.top + (isTitleLabelShowing ? CGRectGetMaxY(self.titleLabel.frame) + titleEdgeInsets.bottom : 0);
+ if (alignLeft) {
+ secondLineMinX = contentOffsetLeft;
+ } else if (alignRight) {
+ secondLineMinX = MAX(contentOffsetLeft, contentOffsetLeft + contentSize.width - secondLineWidth);
+ } else {
+ secondLineMinX = contentOffsetLeft + MAX(0, CGFloatGetCenter(contentSize.width, secondLineWidth));
}
-
- self.subtitleLabel.frame = CGRectFlatMake(subtitleMinX, (isTitleLabelShowing ? CGRectGetMaxY(self.titleLabel.frame) + titleEdgeInsets.bottom : 0) + subtitleEdgeInsets.top, subtitleWidth, self.subtitleLabelSize.height);
-
+ secondLineMaxX = secondLineMinX + MIN(secondLineWidth, contentSize.width);
+ secondLineMinX += self.needsSubAccessoryPlaceholderSpace ? [self subAccessorySpacingSize].width : 0;
if (isSubAccessoryViewShowing) {
- self.subAccessoryView.frame = CGRectSetXY(self.subAccessoryView.frame, CGRectGetMaxX(self.subtitleLabel.frame) + self.subAccessoryViewOffset.x, CGRectGetMinYVerticallyCenter(self.subtitleLabel.frame, self.subAccessoryView.frame) + self.subAccessoryViewOffset.y);
+ self.subAccessoryView.frame = CGRectSetXY(self.subAccessoryView.frame, secondLineMaxX - CGRectGetWidth(self.subAccessoryView.frame), secondLineMinY + CGFloatGetCenter(self.subtitleLabelSize.height, CGRectGetHeight(self.subAccessoryView.frame)) + self.subAccessoryViewOffset.y);
+ secondLineMaxX = CGRectGetMinX(self.subAccessoryView.frame) - self.subAccessoryViewOffset.x;
}
+ self.subtitleLabel.frame = CGRectFlatMake(secondLineMinX, secondLineMinY, secondLineMaxX - secondLineMinX, self.subtitleLabelSize.height);
} else {
self.subtitleLabel.frame = CGRectZero;
}
} else {
+ CGFloat minX = contentOffsetLeft + (self.needsAccessoryPlaceholderSpace ? accessoryViewSpace : 0);
+ CGFloat maxX = maxSize.width - contentOffsetRight - (self.needsLoadingPlaceholderSpace ? loadingViewSpace : 0);
if (self.loadingView) {
- self.loadingView.frame = CGRectSetXY(self.loadingView.frame, minX, CGFloatGetCenter(maxSize.height, self.loadingViewSize.height));
- minX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight;
+ if (self.needsLoadingPlaceholderSpace || !self.loadingView.hidden) {
+ self.loadingView.frame = CGRectSetXY(self.loadingView.frame, minX, CGFloatGetCenter(maxSize.height, self.loadingViewSize.height));
+ minX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight;
+ }
}
if (accessoryView) {
accessoryView.frame = CGRectSetXY(accessoryView.frame, maxX - CGRectGetWidth(accessoryView.bounds), CGFloatGetCenter(maxSize.height, CGRectGetHeight(accessoryView.bounds)) + self.accessoryViewOffset.y);
@@ -362,7 +342,7 @@ - (void)layoutSubviews {
// 如果当前的 contentSize 就是以这个 label 的最大占位计算出来的,那么就不应该先计算 center 再计算偏移
BOOL shouldSubtitleLabelCenterVertically = self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(subtitleEdgeInsets) < contentSize.height;
CGFloat subtitleMinY = shouldSubtitleLabelCenterVertically ? CGFloatGetCenter(maxSize.height, self.subtitleLabelSize.height) + subtitleEdgeInsets.top - subtitleEdgeInsets.bottom : subtitleEdgeInsets.top;
- self.subtitleLabel.frame = CGRectFlatMake(maxX - self.subtitleLabelSize.width, subtitleMinY, self.subtitleLabelSize.width, self.subtitleLabelSize.height);
+ self.subtitleLabel.frame = CGRectFlatMake(MAX(minX + subtitleEdgeInsets.left, maxX - self.subtitleLabelSize.width), subtitleMinY, MIN(self.subtitleLabelSize.width, maxX - minX - subtitleEdgeInsets.left), self.subtitleLabelSize.height);
maxX = CGRectGetMinX(self.subtitleLabel.frame) - subtitleEdgeInsets.left;
} else {
self.subtitleLabel.frame = CGRectZero;
@@ -402,6 +382,11 @@ - (void)setMaximumWidth:(CGFloat)maximumWidth {
[self refreshLayout];
}
+- (void)setPadding:(UIEdgeInsets)padding {
+ _padding = padding;
+ [self refreshLayout];
+}
+
- (void)setContentHorizontalAlignment:(UIControlContentHorizontalAlignment)contentHorizontalAlignment {
[super setContentHorizontalAlignment:contentHorizontalAlignment];
[self refreshLayout];
@@ -441,7 +426,6 @@ - (void)setHorizontalTitleFont:(UIFont *)horizontalTitleFont {
_horizontalTitleFont = horizontalTitleFont;
if (self.style == QMUINavigationTitleViewStyleDefault) {
self.titleLabel.font = horizontalTitleFont;
- [self updateTitleLabelSize];
[self refreshLayout];
}
}
@@ -450,7 +434,6 @@ - (void)setHorizontalSubtitleFont:(UIFont *)horizontalSubtitleFont {
_horizontalSubtitleFont = horizontalSubtitleFont;
if (self.style == QMUINavigationTitleViewStyleDefault) {
self.subtitleLabel.font = horizontalSubtitleFont;
- [self updateSubtitleLabelSize];
[self refreshLayout];
}
}
@@ -459,7 +442,6 @@ - (void)setVerticalTitleFont:(UIFont *)verticalTitleFont {
_verticalTitleFont = verticalTitleFont;
if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) {
self.titleLabel.font = verticalTitleFont;
- [self updateTitleLabelSize];
[self refreshLayout];
}
}
@@ -468,7 +450,6 @@ - (void)setVerticalSubtitleFont:(UIFont *)verticalSubtitleFont {
_verticalSubtitleFont = verticalSubtitleFont;
if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) {
self.subtitleLabel.font = verticalSubtitleFont;
- [self updateSubtitleLabelSize];
[self refreshLayout];
}
}
@@ -486,15 +467,12 @@ - (void)setSubtitleEdgeInsets:(UIEdgeInsets)subtitleEdgeInsets {
- (void)setTitle:(NSString *)title {
_title = title;
self.titleLabel.text = title;
- [self updateTitleLabelSize];
[self refreshLayout];
}
- (void)setSubtitle:(NSString *)subtitle {
_subtitle = subtitle;
self.subtitleLabel.text = subtitle;
- [self updateSubtitleLabelSize];
- [self updateSubAccessoryViewHidden];
[self refreshLayout];
}
@@ -529,7 +507,7 @@ - (void)setAccessoryType:(QMUINavigationTitleViewAccessoryType)accessoryType {
// 经过上面的 setImage 和 sizeToFit 之后再 addSubview,因为 addSubview 会触发系统来询问你的 sizeThatFits:
if (self.accessoryTypeView.superview != self) {
- [self addSubview:self.accessoryTypeView];
+ [self.contentView addSubview:self.accessoryTypeView];
}
[self refreshLayout];
@@ -544,7 +522,7 @@ - (void)setAccessoryView:(UIView *)accessoryView {
_accessoryView = accessoryView;
self.accessoryType = QMUINavigationTitleViewAccessoryTypeNone;
[self.accessoryView sizeToFit];
- [self addSubview:self.accessoryView];
+ [self.contentView addSubview:self.accessoryView];
}
[self refreshLayout];
}
@@ -557,15 +535,14 @@ - (void)setSubAccessoryView:(UIView *)subAccessoryView {
if (subAccessoryView) {
_subAccessoryView = subAccessoryView;
[self.subAccessoryView sizeToFit];
- [self addSubview:self.subAccessoryView];
+ [self.contentView addSubview:self.subAccessoryView];
}
- [self updateSubAccessoryViewHidden];
[self refreshLayout];
}
- (void)updateSubAccessoryViewHidden {
- if (self.subAccessoryView && self.subtitle.length && self.style == QMUINavigationTitleViewStyleSubTitleVertical) {
+ if (self.subAccessoryView && self.subtitleLabel.text.length && self.style == QMUINavigationTitleViewStyleSubTitleVertical) {
self.subAccessoryView.hidden = NO;
} else {
self.subAccessoryView.hidden = YES;
@@ -576,10 +553,11 @@ - (void)setNeedsLoadingView:(BOOL)needsLoadingView {
_needsLoadingView = needsLoadingView;
if (needsLoadingView) {
if (!self.loadingView) {
- _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:NavBarActivityIndicatorViewStyle size:self.loadingViewSize];
+ _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:NavBarActivityIndicatorViewStyle];
+ self.loadingView.qmui_size = self.loadingViewSize;
self.loadingView.color = self.tintColor;
[self.loadingView stopAnimating];
- [self addSubview:self.loadingView];
+ [self.contentView addSubview:self.loadingView];
}
} else {
if (self.loadingView) {
@@ -599,6 +577,14 @@ - (void)setLoadingViewHidden:(BOOL)loadingViewHidden {
[self refreshLayout];
}
+- (void)setLoadingViewSize:(CGSize)loadingViewSize {
+ _loadingViewSize = loadingViewSize;
+ if (self.loadingView) {
+ self.loadingView.qmui_size = loadingViewSize;
+ [self refreshLayout];
+ }
+}
+
- (void)setActive:(BOOL)active {
_active = active;
if ([self.delegate respondsToSelector:@selector(didChangedActive:forTitleView:)]) {
@@ -625,36 +611,41 @@ - (void)setStyle:(QMUINavigationTitleViewStyle)style {
_style = style;
if (style == QMUINavigationTitleViewStyleSubTitleVertical) {
self.titleLabel.font = self.verticalTitleFont;
- [self updateTitleLabelSize];
-
self.subtitleLabel.font = self.verticalSubtitleFont;
- [self updateSubtitleLabelSize];
} else {
self.titleLabel.font = self.horizontalTitleFont;
- [self updateTitleLabelSize];
-
self.subtitleLabel.font = self.horizontalSubtitleFont;
- [self updateSubtitleLabelSize];
}
- [self updateSubAccessoryViewHidden];
[self refreshLayout];
}
- (void)tintColorDidChange {
[super tintColorDidChange];
- UIColor *color = self.tintColor;
- self.titleLabel.textColor = color;
- self.subtitleLabel.textColor = color;
- self.loadingView.color = color;
+ if (self.adjustsSubviewsTintColorAutomatically) {
+ UIColor *color = self.tintColor;
+ self.titleLabel.textColor = color;
+ self.subtitleLabel.textColor = color;
+ self.loadingView.color = color;
+ }
}
#pragma mark - Events
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
- self.alpha = highlighted ? UIControlHighlightedAlpha : 1;
+ if (self.adjustsSubviewsWhenHighlighted) {
+ self.alpha = highlighted ? UIControlHighlightedAlpha : 1;
+ }
+}
+
+- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
+ UIView *result = [super hitTest:point withEvent:event];
+ if (result == self.contentView) {
+ return self;
+ }
+ return result;
}
- (void)handleTouchTitleViewEvent {
@@ -666,6 +657,60 @@ - (void)handleTouchTitleViewEvent {
[self refreshLayout];
}
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+
+ // 让 -[UIViewController setTitle:] 可以自动刷新 QMUINavigationTitle
+ OverrideImplementation([UIViewController class], @selector(setTitle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UIViewController *selfObject, NSString *title) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, NSString *);
+ originSelectorIMP = (void (*)(id, SEL, NSString *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, title);
+
+ if ([selfObject.navigationItem.titleView isKindOfClass:QMUINavigationTitleView.class]) {
+ ((QMUINavigationTitleView *)selfObject.navigationItem.titleView).title = title;
+ }
+ };
+ });
+
+ // 让 -[UINavigationItem setTitle:] 可以自动刷新 QMUINavigationTitleView
+ OverrideImplementation([UINavigationItem class], @selector(setTitle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationItem *selfObject, NSString *title) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, NSString *);
+ originSelectorIMP = (void (*)(id, SEL, NSString *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, title);
+
+ if ([selfObject.titleView isKindOfClass:QMUINavigationTitleView.class]) {
+ ((QMUINavigationTitleView *)selfObject.titleView).title = title;
+ }
+ };
+ });
+
+ // 在先设置了 title 再设置 titleView 时,保证 titleView 的 title 能正确。
+ OverrideImplementation([UINavigationItem class], @selector(setTitleView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationItem *selfObject, QMUINavigationTitleView *titleView) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIView *);
+ originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, titleView);
+
+ if ([titleView isKindOfClass:QMUINavigationTitleView.class]) {
+ if (titleView.title.length <= 0) {
+ NSString *title = selfObject.qmui_viewController.title ?: selfObject.title;
+ titleView.title = title;
+ }
+ }
+ };
+ });
+ });
+}
+
@end
@interface QMUINavigationTitleView (UIAppearance)
@@ -683,6 +728,8 @@ + (void)initialize {
+ (void)setDefaultAppearance {
QMUINavigationTitleView *appearance = [QMUINavigationTitleView appearance];
+ appearance.adjustsSubviewsTintColorAutomatically = YES;
+ appearance.adjustsSubviewsWhenHighlighted = YES;
appearance.maximumWidth = CGFLOAT_MAX;
appearance.loadingViewSize = CGSizeMake(18, 18);
appearance.loadingViewMarginRight = 3;
@@ -697,3 +744,115 @@ + (void)setDefaultAppearance {
}
@end
+
+#pragma mark - LargeTitle 兼容
+
+@implementation QMUINavigationTitleView (LargeTitleCompatibility)
+
+- (void)setAlpha:(BOOL)alpha animated:(BOOL)animated {
+ // 在 push 和 pop 过渡期间系统会对自定义的 titleView 的 alpha 进行调整,了避免和系统的设置冲突(比如设置 alpha 为 0 又被系统还原为 1)这里通过设置 contentView 的 alpha 来控制整个 QMUINavigationTitleView 显示与隐藏。
+ [UIView qmui_animateWithAnimated:animated duration:0.25f animations:^{
+ self.contentView.alpha = alpha;
+ }];
+}
+
+@end
+
+@implementation UINavigationBar (LargeTitleCompatibility)
+
+- (UIView *)qmui_largeTitleView {
+ for (UIView *subview in self.subviews) {
+ if ([NSStringFromClass(subview.class) hasSuffix:@"LargeTitleView"]) {
+ return subview;
+ }
+ }
+ return nil;
+}
+
+@end
+
+
+@implementation UINavigationController(LargeTitleCompatibility)
+
++ (void)load {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ // -[UINavigationController _updateTopViewFramesToMatchScrollOffsetInViewController:contentScrollView:topLayoutType:]
+ OverrideImplementation([UINavigationController class], sel_registerName("_updateTopViewFramesToMatchScrollOffsetInViewController:contentScrollView:topLayoutType:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationController *selfObject, UIViewController *viewController, UIScrollView *scrollView, NSUInteger topLayoutType) {
+
+ // call super
+ void (*originSelectorIMP)(id, SEL, UIViewController *, UIScrollView *, NSUInteger);
+ originSelectorIMP = (void (*)(id, SEL, UIViewController *, UIScrollView *, NSUInteger))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD, viewController, scrollView, topLayoutType);
+
+ [selfObject qmui_updateTitleViewToMatchScrollOffsetInViewController:viewController contentScrollView:scrollView topLayoutType:topLayoutType];
+ };
+ });
+ });
+}
+
+- (void)qmui_updateTitleViewToMatchScrollOffsetInViewController:(UIViewController *)viewController contentScrollView:(UIScrollView *)contentScrollView topLayoutType:(NSInteger)topLayoutType {
+ UIView *titleView = viewController.navigationItem.titleView;
+ if (!titleView || ![titleView isKindOfClass:[QMUINavigationTitleView class]]) {
+ return;
+ }
+
+ if (viewController.navigationController != self) return;
+
+ QMUINavigationTitleView *navigationTitleView = (QMUINavigationTitleView *)titleView;
+ UIView *largeTitleView = self.navigationBar.qmui_largeTitleView;
+ BOOL largeTitleLabelVisable = self.navigationBar.prefersLargeTitles && viewController.qmui_prefersLargeTitleDisplayed && largeTitleView.alpha != 0;
+ BOOL titleViewAlpha = largeTitleLabelVisable ? 0 : 1;
+ BOOL animated = contentScrollView.layer.presentationLayer && !CGRectEqualToRect(contentScrollView.layer.presentationLayer.bounds, contentScrollView.layer.bounds);
+ [navigationTitleView setAlpha:titleViewAlpha animated:animated];
+}
+
+
+@end
+
+@implementation UIView (QMUINavigationTitleView)
+
+static char kAssociatedObjectKey_useAsNavigationTitleView;
+- (void)setQmui_useAsNavigationTitleView:(BOOL)useAsNavigationTitleView {
+ objc_setAssociatedObject(self, &kAssociatedObjectKey_useAsNavigationTitleView, @(useAsNavigationTitleView), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+ if (useAsNavigationTitleView) {
+ [QMUIHelper executeBlock:^{
+ // 修复系统使用自定义 titleView 时的布局问题
+ OverrideImplementation([UINavigationBar class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+ return ^(UINavigationBar *selfObject) {
+
+ UIView *titleView = selfObject.topItem.titleView;
+
+ if (titleView.qmui_useAsNavigationTitleView) {
+ CGFloat titleViewMaximumWidth = CGRectGetWidth(titleView.bounds);// 初始状态下titleView会被设置为UINavigationBar允许的最大宽度
+ CGSize titleViewSize = [titleView sizeThatFits:CGSizeMake(titleViewMaximumWidth, CGFLOAT_MAX)];
+ titleViewSize.height = ceil(titleViewSize.height);// titleView的高度如果非pt整数,会导致计算出来的y值时多时少,所以干脆做一下pt取整,这个策略不要改,改了要重新测试push过程中titleView是否会跳动
+
+ // 当在UINavigationBar里使用自定义的titleView时,就算titleView的sizeThatFits:返回正确的高度,navigationBar也不会帮你设置高度(但会帮你设置宽度),所以我们需要自己更新高度并且修正y值
+ if (CGRectGetHeight(titleView.bounds) != titleViewSize.height) {
+ CGFloat titleViewMinY = flat(CGRectGetMinY(titleView.frame) - ((titleViewSize.height - CGRectGetHeight(titleView.bounds)) / 2.0));// 系统对titleView的y值布局是flat,注意,不能改,改了要测试
+ titleView.frame = CGRectMake(CGRectGetMinX(titleView.frame), titleViewMinY, MIN(titleViewMaximumWidth, titleViewSize.width), titleViewSize.height);
+ }
+
+ // iOS 11 之后(iOS 11 Beta 5 测试过) titleView 的布局发生了一些变化,如果不主动设置宽度,titleView 里的内容就可能无法完整展示
+ if (CGRectGetWidth(titleView.bounds) != titleViewSize.width) {
+ titleView.frame = CGRectSetWidth(titleView.frame, titleViewSize.width);
+ }
+ }
+
+ // call super
+ void (*originSelectorIMP)(id, SEL);
+ originSelectorIMP = (void (*)(id, SEL))originalIMPProvider();
+ originSelectorIMP(selfObject, originCMD);
+ };
+ });
+ } oncePerIdentifier:@"UIView (QMUINavigationTitleView)"];
+ }
+}
+
+- (BOOL)qmui_useAsNavigationTitleView {
+ return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_useAsNavigationTitleView)) boolValue];
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h
index 6e5af72a..09f30fdf 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIOrderedDictionary.h
@@ -16,13 +16,18 @@
#import
#import
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ 一个简单实现的有序的 key-value 容器,通过 initWithKeysAndObjects: 初始化后,用下标访问即可,如 dict[0] 或 dict[key]
+ */
@interface QMUIOrderedDictionary<__covariant KeyType, __covariant ObjectType> : NSObject
- (instancetype)initWithKeysAndObjects:(id)firstKey,...;
@property(readonly) NSUInteger count;
@property(nonatomic, copy, readonly) NSArray *allKeys;
-
+@property(nonatomic, copy, readonly) NSArray *allValues;
- (void)setObject:(ObjectType)object forKey:(KeyType)key;
- (void)addObject:(ObjectType)object forKey:(KeyType)key;
- (void)addObjects:(NSArray *)objects forKeys:(NSArray *)keys;
@@ -30,7 +35,13 @@
- (void)insertObjects:(NSArray *)objects forKeys:(NSArray *)keys atIndex:(NSInteger)index;
- (void)removeObject:(ObjectType)object forKey:(KeyType)key;
- (void)removeObject:(ObjectType)object atIndex:(NSInteger)index;
-- (ObjectType)objectForKey:(KeyType)key;
+- (nullable ObjectType)objectForKey:(KeyType)key;
- (ObjectType)objectAtIndex:(NSInteger)index;
+// 支持下标的方式访问,需要声明以下两个方法
+- (nullable ObjectType)objectForKeyedSubscript:(KeyType)key;
+- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx;
+
@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m
index 1ad98aaa..c17c093c 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIOrderedDictionary.m
@@ -69,6 +69,10 @@ - (NSArray *)allKeys {
return self.mutableAllKeys.copy;
}
+- (NSArray *)allValues {
+ return self.mutableAllValues.copy;
+}
+
- (void)setObject:(id)object forKey:(id)key {
if ([self.mutableAllKeys containsObject:key]) {
NSInteger index = [self.mutableAllKeys indexOfObject:key];
@@ -132,10 +136,18 @@ - (id)objectForKey:(id)key {
return [self.mutableDictionary objectForKey:key];
}
+- (id)objectForKeyedSubscript:(id)key {
+ return [self objectForKey:key];
+}
+
- (id)objectAtIndex:(NSInteger)index {
return [self.mutableDictionary objectForKey:self.mutableAllKeys[index]];
}
+- (id)objectAtIndexedSubscript:(NSUInteger)idx {
+ return [self objectAtIndex:idx];
+}
+
- (NSString *)description {
return [NSString stringWithFormat:@"%@, %@", [super description], self.mutableDictionary];
}
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h
index e84e0206..2ba4fae5 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIPieProgressView.h
@@ -24,6 +24,12 @@
*
* 通过 `UIControlEventValueChanged` 来监听进度变化
*/
+
+typedef NS_ENUM(NSUInteger, QMUIPieProgressViewShape) {
+ QMUIPieProgressViewShapeSector, // 扇形,默认
+ QMUIPieProgressViewShapeRing // 环形
+};
+
@interface QMUIPieProgressView : UIControl
/**
@@ -46,6 +52,16 @@
*/
@property(nonatomic, assign) IBInspectable CGFloat borderInset;
+/**
+ 线宽,用于环形绘制,默认为 0。
+ */
+@property(nonatomic, assign) IBInspectable CGFloat lineWidth;
+
+/**
+ 绘制形状,默认是扇形。
+ */
+@property(nonatomic, assign) IBInspectable QMUIPieProgressViewShape shape;
+
/**
修改当前的进度,会触发 UIControlEventValueChanged 事件
@@ -53,4 +69,5 @@
@param animated 是否以动画来表现
*/
- (void)setProgress:(float)progress animated:(BOOL)animated;
+
@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m
index dfd509f2..605bf521 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIPieProgressView.m
@@ -19,16 +19,24 @@
@interface QMUIPieProgressLayer : CALayer
@property(nonatomic, strong) UIColor *fillColor;
+@property(nonatomic, strong) UIColor *strokeColor;
@property(nonatomic, assign) float progress;
@property(nonatomic, assign) CFTimeInterval progressAnimationDuration;
@property(nonatomic, assign) BOOL shouldChangeProgressWithAnimation; // default is YES
@property(nonatomic, assign) CGFloat borderInset;
+@property(nonatomic, assign) CGFloat lineWidth;
+@property(nonatomic, assign) QMUIPieProgressViewShape shape;
+
@end
@implementation QMUIPieProgressLayer
// 加dynamic才能让自定义的属性支持动画
@dynamic fillColor;
+@dynamic strokeColor;
@dynamic progress;
+@dynamic shape;
+@dynamic lineWidth;
+@dynamic borderInset;
- (instancetype)init {
if (self = [super init]) {
@@ -56,23 +64,41 @@ - (void)drawInContext:(CGContextRef)context {
return;
}
- // 绘制扇形进度区域
CGPoint center = CGPointGetCenterWithRect(self.bounds);
CGFloat radius = MIN(center.x, center.y) - self.borderWidth - self.borderInset;
CGFloat startAngle = -M_PI_2;
CGFloat endAngle = M_PI * 2 * self.progress + startAngle;
- CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
- CGContextMoveToPoint(context, center.x, center.y);
- CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0);
- CGContextClosePath(context);
- CGContextFillPath(context);
+
+ switch (self.shape) {
+ case QMUIPieProgressViewShapeSector: {
+ // 绘制扇形进度区域
+
+ CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
+ CGContextMoveToPoint(context, center.x, center.y);
+ CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0);
+ CGContextClosePath(context);
+ CGContextFillPath(context);
+ }
+ break;
+
+ case QMUIPieProgressViewShapeRing: {
+ // 绘制环形进度区域
+
+ radius -= self.lineWidth;
+ CGContextSetLineWidth(context, self.lineWidth);
+ CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor);
+ CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0);
+ CGContextStrokePath(context);
+ }
+ break;
+ }
[super drawInContext:context];
}
-- (void)setFrame:(CGRect)frame {
- [super setFrame:frame];
- self.cornerRadius = CGRectGetHeight(frame) / 2;
+- (void)layoutSublayers {
+ [super layoutSublayers];
+ self.cornerRadius = CGRectGetHeight(self.bounds) / 2;
}
@end
@@ -118,9 +144,8 @@ - (void)setProgress:(float)progress {
- (void)setProgress:(float)progress animated:(BOOL)animated {
_progress = fmax(0.0, fmin(1.0, progress));
- QMUIPieProgressLayer *layer = (QMUIPieProgressLayer *)self.layer;
- layer.shouldChangeProgressWithAnimation = animated;
- layer.progress = _progress;
+ self.progressLayer.shouldChangeProgressWithAnimation = animated;
+ self.progressLayer.progress = _progress;
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
@@ -132,7 +157,7 @@ - (void)setProgressAnimationDuration:(CFTimeInterval)progressAnimationDuration {
- (void)setBorderWidth:(CGFloat)borderWidth {
_borderWidth = borderWidth;
- self.layer.borderWidth = borderWidth;
+ self.progressLayer.borderWidth = borderWidth;
}
- (void)setBorderInset:(CGFloat)borderInset {
@@ -140,12 +165,24 @@ - (void)setBorderInset:(CGFloat)borderInset {
self.progressLayer.borderInset = borderInset;
}
+- (void)setLineWidth:(CGFloat)lineWidth {
+ _lineWidth = lineWidth;
+ self.progressLayer.lineWidth = lineWidth;
+}
+
- (void)tintColorDidChange {
[super tintColorDidChange];
self.progressLayer.fillColor = self.tintColor;
+ self.progressLayer.strokeColor = self.tintColor;
self.progressLayer.borderColor = self.tintColor.CGColor;
}
+- (void)setShape:(QMUIPieProgressViewShape)shape {
+ _shape = shape;
+ self.progressLayer.shape = shape;
+ [self setBorderWidth:_borderWidth];
+}
+
- (QMUIPieProgressLayer *)progressLayer {
return (QMUIPieProgressLayer *)self.layer;
}
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h
index 38cddbca..2da5a690 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIPopupContainerView.h
@@ -16,13 +16,23 @@
#import
#import "UIControl+QMUI.h"
+NS_ASSUME_NONNULL_BEGIN
+
typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
QMUIPopupContainerViewLayoutDirectionAbove,
- QMUIPopupContainerViewLayoutDirectionBelow
+ QMUIPopupContainerViewLayoutDirectionBelow,
+ QMUIPopupContainerViewLayoutDirectionLeft,
+ QMUIPopupContainerViewLayoutDirectionRight
+};
+
+typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutAlignment) {
+ QMUIPopupContainerViewLayoutAlignmentCenter,
+ QMUIPopupContainerViewLayoutAlignmentLeading,
+ QMUIPopupContainerViewLayoutAlignmentTrailing,
};
/**
- * 带箭头的小tips浮层,自带 imageView 和 textLabel,可展示简单的图文信息。
+ * 带箭头的小tips浮层,自带 imageView 和 textLabel,可展示简单的图文信息,支持 UIViewContentModeTop/UIViewContentModeBottom/UIViewContentModeCenter 三种布局方式。
* QMUIPopupContainerView 支持以两种方式显示在界面上:
* 1. 添加到某个 UIView 上(适合于 viewController 切换时浮层跟着一起切换的场景),这种场景只能手动隐藏浮层。
* 2. 在 QMUIPopupContainerView 自带的 UIWindow 里显示(适合于用完就消失的场景,不要涉及界面切换),这种场景支持点击空白地方自动隐藏浮层。
@@ -44,10 +54,13 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
* 3. 通过重写 sizeThatFitsInContentView:,在里面返回当前 subviews 的大小。
* 4. 在 layoutSubviews: 里,所有 subviews 请相对于 contentView 布局。
*/
-
@interface QMUIPopupContainerView : UIControl {
CAShapeLayer *_backgroundLayer;
+ CAShapeLayer *_borderLayer;// CAShapeLayer 的特性是有一半 stroke 会和 fill 重叠,而我们希望的是 stroke 在 fill 外面,所以只能分开两个 layer 实现 border 和 background
+ UIImageView *_arrowImageView;
CGFloat _arrowMinX;
+ CGFloat _arrowMinY;
+ BOOL _shouldInvalidateLayout;
}
@property(nonatomic, assign) BOOL debug;
@@ -56,14 +69,23 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
/// 默认为 NO,也即需要手动调用代码去隐藏浮层。
@property(nonatomic, assign) BOOL automaticallyHidesWhenUserTap;
-/// 所有subview都应该添加到contentView上,默认contentView.userInteractionEnabled = NO,需要事件操作时自行打开
+/// 所有 subview 都应该添加到 contentView 上,subviews 占据的大小通过 sizeThatFitsInContentView: 或者 contentViewSizeThatFitsBlock 来返回,subviews 的布局通过重写 layoutSubviews 或 qmui_layoutSubviewsBlock 来布局,注意布局时应该基于 contentView。
@property(nonatomic, strong, readonly) UIView *contentView;
+/**
+ 与 sizeThatFitsInContentView: 等价,用于告诉组件,添加到 contentView 上的 subviews 的大小
+
+ @param size 浮层里除去 safetyMarginsOfSuperview、arrowSize、contentEdgeInsets 之外后,留给内容的实际大小,计算 subview 大小时均应使用这个参数来计算
+ @return 自定义内容实际占据的大小
+ @note 计算结果不需要操心 maximumWidth、minimumWidth,这些会由组件统一处理,你只需要在这个 block 里返回内容的实际大小即可。
+ */
+@property(nonatomic, copy, nullable) CGSize (^contentViewSizeThatFitsBlock)(CGSize size);
+
/// 预提供的UIImageView,默认为nil,调用到的时候才初始化
-@property(nonatomic, strong, readonly) UIImageView *imageView;
+@property(nonatomic, strong, readonly, nullable) UIImageView *imageView;
/// 预提供的UILabel,默认为nil,调用到的时候才初始化。默认支持多行。
-@property(nonatomic, strong, readonly) UILabel *textLabel;
+@property(nonatomic, strong, readonly, nullable) UILabel *textLabel;
/// 圆角矩形气泡内的padding(不包括三角箭头),默认是(8, 8, 8, 8)
@property(nonatomic, assign) UIEdgeInsets contentEdgeInsets UI_APPEARANCE_SELECTOR;
@@ -77,13 +99,18 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
/// 三角箭头的大小,默认为 CGSizeMake(18, 9)
@property(nonatomic, assign) CGSize arrowSize UI_APPEARANCE_SELECTOR;
+/// 三角箭头的图片,通常用于默认的三角样式不满足需求时。当使用了 arrowImage 后,arrowSize 将会被固定为 arrowImage.size。
+/// 当 borderWidth 大于0时,arrowImage 会与所在那一侧的 border 重叠,所以你的切图需要预留一部分 borderWidth 的区域以盖住边框。
+/// 图片必须为箭头向下的方向
+@property(nonatomic, strong, nullable) UIImage *arrowImage UI_APPEARANCE_SELECTOR;
+
/// 最大宽度(指整个控件的宽度,而不是contentView部分),默认为CGFLOAT_MAX
@property(nonatomic, assign) CGFloat maximumWidth UI_APPEARANCE_SELECTOR;
/// 最小宽度(指整个控件的宽度,而不是contentView部分),默认为0
@property(nonatomic, assign) CGFloat minimumWidth UI_APPEARANCE_SELECTOR;
-/// 最大高度(指整个控件的高度,而不是contentView部分),默认为CGFLOAT_MAX
+/// 最大高度(指整个控件的高度,而不是contentView部分),默认为CGFLOAT_MAX,会在布局时被动态修改。
@property(nonatomic, assign) CGFloat maximumHeight UI_APPEARANCE_SELECTOR;
/// 最小高度(指整个控件的高度,而不是contentView部分),默认为0
@@ -95,58 +122,99 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
/// 最终的布局方向(preferLayoutDirection只是期望的方向,但有可能那个方向已经没有剩余空间可摆放控件了,所以会自动变换)
@property(nonatomic, assign, readonly) QMUIPopupContainerViewLayoutDirection currentLayoutDirection;
+/// 计算布局时期望浮层与目标位置的对齐方式,默认为 QMUIPopupContainerViewLayoutAlignmentCenter,也即浮层和目标位置相对居中。
+/// 对 preferLayoutDirection 为 Above/Below 而言,Leading 表示浮层的左侧与目标位置左边缘对齐,Trailing 表示浮层的右侧与目标位置右边缘对齐。
+/// 对 preferLayoutDirection 为 Left/Right 而言,Leading 表示浮层的顶端与目标位置顶边缘对齐,Trailing 表示浮层的底端与目标位置底边缘对齐。
+/// 如果预期的对齐方式无法被满足时,会根据 usesOppositeLayoutAlignmentIfNeeded 的值来决定备选方案。
+@property(nonatomic, assign) QMUIPopupContainerViewLayoutAlignment preferLayoutAlignment UI_APPEARANCE_SELECTOR;
+
+/// 表示 preferLayoutAlignment 在极端情况下无法满足调用方设置的值时,应该以什么方式作为备选。
+/// 若当前属性值为 YES,则表示用相反的对齐方式去尝试(例如 preferLayoutAlignment = QMUIPopupContainerViewLayoutAlignmentLeading 则在极端情况下会用 QMUIPopupContainerViewLayoutAlignmentTrailing 作为备选),若当前属性值为 NO 则表示保持对齐方向不变,让浮层的边缘紧贴着 safetyMarginsOfSuperview 即可。
+/// 默认为 YES。
+/// @warning 对 QMUIPopupContainerViewLayoutAlignmentCenter 无意义,因为 QMUIPopupContainerViewLayoutAlignmentCenter 没有所谓的相反概念。
+@property(nonatomic, assign) BOOL usesOppositeLayoutAlignmentIfNeeded UI_APPEARANCE_SELECTOR;
+
/// 最终布局时箭头距离目标边缘的距离,默认为5
@property(nonatomic, assign) CGFloat distanceBetweenSource UI_APPEARANCE_SELECTOR;
-/// 最终布局时与父节点的边缘的临界点,默认为(10, 10, 10, 10)
+/// 最终布局时与父节点的边缘的临界点,默认为(10, 10, 10, 10),注意这里的值不需要由业务考虑 safeAreaInsets,内部会自己叠加。
@property(nonatomic, assign) UIEdgeInsets safetyMarginsOfSuperview UI_APPEARANCE_SELECTOR;
-@property(nonatomic, strong) UIColor *backgroundColor UI_APPEARANCE_SELECTOR;
-@property(nonatomic, strong) UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR;
+/// 允许用一个自定的 view 作为背景,会自动将其 mask 为圆角带箭头的造型,当同时使用 backgroundView 和 arrowImage 时,arrowImage 只作为遮罩使用(也即使用它的造型,不显示它的图片内容)。
+/// 默认为 nil。
+@property(nonatomic, strong, nullable) UIView *backgroundView;
+
+/// 浮层的背景色,作用区域为箭头+圆角矩形区域,当同时使用 backgroundView 和 backgroundColor 时,backgroundView 会盖在 backgroundColor 上方。
+@property(nonatomic, strong, nullable) UIColor *backgroundColor UI_APPEARANCE_SELECTOR;
+
+/// 浮层点击 highlighted 时的背景色,作用区域为箭头+圆角矩形区域
+@property(nonatomic, strong, nullable) UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR;
/// 当使用方法 2 显示并且打开了 automaticallyHidesWhenUserTap 时,可修改背景遮罩的颜色,默认为 UIColorMask,若非使用方法 2,或者没有打开 automaticallyHidesWhenUserTap,则背景遮罩为透明(可视为不存在背景遮罩)
-@property(nonatomic, strong) UIColor *maskViewBackgroundColor UI_APPEARANCE_SELECTOR;
-@property(nonatomic, strong) UIColor *shadowColor UI_APPEARANCE_SELECTOR;
-@property(nonatomic, strong) UIColor *borderColor UI_APPEARANCE_SELECTOR;
+@property(nonatomic, strong, nullable) UIColor *maskViewBackgroundColor UI_APPEARANCE_SELECTOR;
+
+/// 浮层的阴影,默认包含箭头的形状,如果使用了 @c arrowImage 则不包含箭头。当不需要阴影时可将其置为 nil。
+@property(nonatomic, strong, nullable) NSShadow *shadow UI_APPEARANCE_SELECTOR;
+
+@property(nonatomic, strong, nullable) UIColor *borderColor UI_APPEARANCE_SELECTOR;
@property(nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR;
@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR;
/// 可以是 UINavigationBar、UIToolbar 上的 UIBarButtonItem,或者 UITabBar 上的 UITabBarItem
-@property(nonatomic, weak) __kindof UIBarItem *sourceBarItem;
+@property(nonatomic, weak, nullable) __kindof UIBarItem *sourceBarItem;
-@property(nonatomic, weak) __kindof UIView *sourceView;
+@property(nonatomic, weak, nullable) __kindof UIView *sourceView;
/// rect 需要处于 QMUIPopupContainerView 所在的坐标系内,例如如果 popup 使用 addSubview: 的方式添加到界面,则 sourceRect 应该是 superview 坐标系内的;如果 popup 使用 window 的方式展示,则 sourceRect 需要转换为 window 坐标系内。
@property(nonatomic, assign) CGRect sourceRect;
-/// 立即刷新当前 popup 的布局,前提是 popup 已经被 show 过。
+/// 标记为需要更新布局,会在下一次 runloop 里统一调用 updateLayout。一般情况请用这个方法,避免直接用 updateLayout,从而获取更佳的性能。
+- (void)setNeedsUpdateLayout;
+
+/// 立即刷新当前 popup 的布局,前提是 popup.isShowing 为 YES。
- (void)updateLayout;
- (void)showWithAnimated:(BOOL)animated;
-- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion;
+- (void)showWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion;
- (void)hideWithAnimated:(BOOL)animated;
-- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion;
+- (void)hideWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion;
- (BOOL)isShowing;
+/// 允许业务自定义显示动画,请在这个 block 里写自己的动画实现,并在恰当的时候调用参数里的 animation() 和 completion()。
+/// @param defaultAnimation 组件里默认的显示动画(默认实现是 scale+alpha),业务可自行决定是否要调用它
+/// @param completion 动画结束的回调,业务必须在自己动画结束时主动调用它
+/// @param isWindowMode 是否正在以 window 模式展示
+/// @param rootView popup 组件当前所在的容器,如果是 window 模式,则是内部自己创建的 window.rootViewController.view,若是其他模式则是业务的 view
+/// @param popup 当前 popup 实例
+@property(nonatomic, copy) void (^showingAnimationBlock)(void (^defaultAnimation)(void), void (^completion)(BOOL finished), BOOL isWindowMode, UIView * _Nullable rootView, __kindof QMUIPopupContainerView *popup);
+
+/// 允许业务自定义隐藏动画,请在这个 block 里写自己的动画实现,并在恰当的时候调用参数里的 animation() 和 completion()。
+/// @param defaultAnimation 组件里默认的显示动画(默认实现是 scale+alpha),业务可自行决定是否要调用它
+/// @param completion 动画结束的回调,业务必须在自己动画结束时主动调用它
+/// @param isWindowMode 是否正在以 window 模式展示
+/// @param rootView popup 组件当前所在的容器,如果是 window 模式,则是内部自己创建的 window.rootViewController.view,若是其他模式则是业务的 view
+/// @param popup 当前 popup 实例
+@property(nonatomic, copy) void (^hidingAnimationBlock)(void (^defaultAnimation)(void), void (^completion)(BOOL finished), BOOL isWindowMode, UIView * _Nullable rootView, __kindof QMUIPopupContainerView *popup);
+
/**
* 即将显示时的回调
* 注:如果需要使用例如 didShowBlock 的时机,请使用 @showWithAnimated:completion: 的 completion 参数来实现。
* @argv animated 是否需要动画
*/
-@property(nonatomic, copy) void (^willShowBlock)(BOOL animated);
+@property(nonatomic, copy, nullable) void (^willShowBlock)(BOOL animated);
/**
* 即将隐藏时的回调
* @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏
* @argv animated 是否需要动画
*/
-@property(nonatomic, copy) void (^willHideBlock)(BOOL hidesByUserTap, BOOL animated);
+@property(nonatomic, copy, nullable) void (^willHideBlock)(BOOL hidesByUserTap, BOOL animated);
/**
* 已经隐藏后的回调
* @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏
*/
-@property(nonatomic, copy) void (^didHideBlock)(BOOL hidesByUserTap);
+@property(nonatomic, copy, nullable) void (^didHideBlock)(BOOL hidesByUserTap);
@end
@@ -156,10 +224,13 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
- (void)didInitialize NS_REQUIRES_SUPER;
/**
- 子类重写,告诉父类subviews的合适大小
+ 子类重写,用于告诉父类添加到 contentView 上的 subviews 的大小
@param size 浮层里除去 safetyMarginsOfSuperview、arrowSize、contentEdgeInsets 之外后,留给内容的实际大小,计算 subview 大小时均应使用这个参数来计算
@return 自定义内容实际占据的大小
+ @note 计算结果不需要操心 maximumWidth、minimumWidth,这些会由组件统一处理,你只需要在这个方法里返回内容的实际大小即可。
*/
- (CGSize)sizeThatFitsInContentView:(CGSize)size;
@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m
index 9d5a9d83..dd76738c 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIPopupContainerView.m
@@ -21,6 +21,9 @@
#import "UIView+QMUI.h"
#import "UIWindow+QMUI.h"
#import "UIBarItem+QMUI.h"
+#import "QMUIAppearance.h"
+#import "CALayer+QMUI.h"
+#import "NSShadow+QMUI.h"
@interface QMUIPopupContainerViewWindow : UIWindow
@@ -35,14 +38,13 @@ @interface QMUIPopContainerMaskControl : UIControl
@property(nonatomic, weak) QMUIPopupContainerView *popupContainerView;
@end
-@interface QMUIPopupContainerView (UIAppearance)
-
-- (void)updateAppearance;
-@end
-
@interface QMUIPopupContainerView () {
UIImageView *_imageView;
UILabel *_textLabel;
+
+ CALayer *_backgroundViewMaskLayer;
+ CAShapeLayer *_copiedBackgroundLayer;
+ CALayer *_copiedArrowImageLayer;
}
@property(nonatomic, strong) QMUIPopupContainerViewWindow *popupWindow;
@@ -98,9 +100,40 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return result;
}
+- (void)setBackgroundView:(UIView *)backgroundView {
+ if (_backgroundView && _backgroundView != backgroundView) {
+ [_backgroundView removeFromSuperview];
+ }
+ _backgroundView = backgroundView;
+ if (backgroundView) {
+ [self insertSubview:backgroundView atIndex:0];
+ // backgroundView 必须盖在 _backgroundLayer、_arrowImageView 上面,否则背景色、阴影、箭头图片都会盖在 backgroundView 上方,影响表现
+ [self sendSubviewToBack:_arrowImageView];
+ [self.layer qmui_sendSublayerToBack:_backgroundLayer];
+ [self.layer qmui_sendSublayerToBack:_borderLayer];
+ if (!_backgroundViewMaskLayer) {
+ _copiedBackgroundLayer = [CAShapeLayer layer];
+ [_copiedBackgroundLayer qmui_removeDefaultAnimations];
+ _copiedBackgroundLayer.fillColor = UIColor.blackColor.CGColor;// 这个 layer 是作为 mask 使用的,所以必须完整填充不透明的颜色,否则会影响 mask 效果
+
+ _copiedArrowImageLayer = [CALayer layer];
+ [_copiedArrowImageLayer qmui_removeDefaultAnimations];
+
+ _backgroundViewMaskLayer = [CALayer layer];
+ [_backgroundViewMaskLayer qmui_removeDefaultAnimations];
+ [_backgroundViewMaskLayer addSublayer:_copiedBackgroundLayer];
+ [_backgroundViewMaskLayer addSublayer:_copiedArrowImageLayer];
+ }
+ backgroundView.layer.mask = _backgroundViewMaskLayer;
+ }
+ // 存在 backgroundView 则隐藏原始的箭头,避免在 backgroundView 背后影响显示
+ _arrowImageView.hidden = backgroundView || !self.arrowImage;
+}
+
- (void)setBackgroundColor:(UIColor *)backgroundColor {
_backgroundColor = backgroundColor;
_backgroundLayer.fillColor = _backgroundColor.CGColor;
+ _arrowImageView.tintColor = backgroundColor;
}
- (void)setMaskViewBackgroundColor:(UIColor *)maskViewBackgroundColor {
@@ -110,19 +143,19 @@ - (void)setMaskViewBackgroundColor:(UIColor *)maskViewBackgroundColor {
}
}
-- (void)setShadowColor:(UIColor *)shadowColor {
- _shadowColor = shadowColor;
- _backgroundLayer.shadowColor = shadowColor.CGColor;
+- (void)setShadow:(NSShadow *)shadow {
+ _shadow = shadow;
+ _borderLayer.qmui_shadow = shadow;
}
- (void)setBorderColor:(UIColor *)borderColor {
_borderColor = borderColor;
- _backgroundLayer.strokeColor = borderColor.CGColor;
+ _borderLayer.strokeColor = borderColor.CGColor;
}
- (void)setBorderWidth:(CGFloat)borderWidth {
_borderWidth = borderWidth;
- _backgroundLayer.lineWidth = _borderWidth;
+ _borderLayer.lineWidth = _borderWidth;
}
- (void)setCornerRadius:(CGFloat)cornerRadius {
@@ -133,104 +166,236 @@ - (void)setCornerRadius:(CGFloat)cornerRadius {
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
if (self.highlightedBackgroundColor) {
- _backgroundLayer.fillColor = highlighted ? self.highlightedBackgroundColor.CGColor : self.backgroundColor.CGColor;
+ UIColor *color = highlighted ? self.highlightedBackgroundColor : self.backgroundColor;
+ _backgroundLayer.fillColor = color.CGColor;
+ _arrowImageView.tintColor = color;
+ }
+}
+
+- (void)setPreferLayoutAlignment:(QMUIPopupContainerViewLayoutAlignment)preferLayoutAlignment {
+ _preferLayoutAlignment = preferLayoutAlignment;
+ if (self.isShowing) {
+ [self setNeedsUpdateLayout];
+ }
+}
+
+- (void)setDistanceBetweenSource:(CGFloat)distanceBetweenSource {
+ _distanceBetweenSource = distanceBetweenSource;
+ if (self.isShowing) {
+ [self setNeedsUpdateLayout];
}
}
- (CGSize)sizeThatFits:(CGSize)size {
- size.width = MIN(size.width, CGRectGetWidth(self.superview.bounds) - UIEdgeInsetsGetHorizontalValue(self.safetyMarginsOfSuperview));
- size.height = MIN(size.height, CGRectGetHeight(self.superview.bounds) - UIEdgeInsetsGetVerticalValue(self.safetyMarginsOfSuperview));
-
CGSize contentLimitSize = [self contentSizeInSize:size];
- CGSize contentSize = [self sizeThatFitsInContentView:contentLimitSize];
+ CGSize contentSize = CGSizeZero;
+ if (self.contentViewSizeThatFitsBlock) {
+ contentSize = self.contentViewSizeThatFitsBlock(contentLimitSize);
+ } else {
+ contentSize = [self sizeThatFitsInContentView:contentLimitSize];
+ }
CGSize resultSize = [self sizeWithContentSize:contentSize sizeThatFits:size];
return resultSize;
}
- (void)layoutSubviews {
[super layoutSubviews];
- CGSize arrowSize = self.arrowSize;
- CGRect roundedRect = CGRectMake(self.borderWidth / 2.0, self.borderWidth / 2.0 + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove ? 0 : arrowSize.height), CGRectGetWidth(self.bounds) - self.borderWidth, CGRectGetHeight(self.bounds) - arrowSize.height - self.borderWidth);
- CGFloat cornerRadius = self.cornerRadius;
+ BOOL isUsingArrowImage = !!self.arrowImage;
+ CGAffineTransform arrowImageTransform = CGAffineTransformIdentity;
+ CGPoint arrowImagePosition = CGPointZero;
+ CGSize arrowSize = self.arrowSizeAuto;
+ if (isUsingArrowImage) {
+ switch (self.currentLayoutDirection) {
+ case QMUIPopupContainerViewLayoutDirectionRight: {
+ arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(90));
+ arrowImagePosition = CGPointMake(arrowSize.width / 2, _arrowMinY + arrowSize.height / 2);
+ }
+ break;
+ case QMUIPopupContainerViewLayoutDirectionAbove: {
+ arrowImagePosition = CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetHeight(self.bounds) - arrowSize.height / 2);
+ }
+ break;
+ case QMUIPopupContainerViewLayoutDirectionLeft: {
+ arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(-90));
+ arrowImagePosition = CGPointMake(CGRectGetWidth(self.bounds) - arrowSize.width / 2, _arrowMinY + arrowSize.height / 2);
+ }
+ break;
+ case QMUIPopupContainerViewLayoutDirectionBelow: {
+ arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(-180));
+ arrowImagePosition = CGPointMake(_arrowMinX + arrowSize.width / 2, arrowSize.height / 2);
+ }
+ break;
+ default:
+ break;
+ }
+ _arrowImageView.transform = arrowImageTransform;
+ _arrowImageView.center = arrowImagePosition;
+ }
+
+ UIBezierPath *borderPath = [self generatePathForBorder:YES];
+ _borderLayer.path = borderPath.CGPath;
+ _borderLayer.shadowPath = borderPath.CGPath;
+ _borderLayer.frame = self.bounds;
+
+ UIBezierPath *backgroundPath = [self generatePathForBorder:NO];
+ _backgroundLayer.path = backgroundPath.CGPath;
+ _backgroundLayer.frame = self.bounds;
+
+ if (self.backgroundView) {
+ self.backgroundView.frame = self.bounds;
+ _backgroundViewMaskLayer.frame = self.bounds;
+
+ _copiedBackgroundLayer.path = _backgroundLayer.path;
+ _copiedBackgroundLayer.frame = _backgroundLayer.frame;
+
+ _copiedArrowImageLayer.bounds = _arrowImageView.bounds;
+ _copiedArrowImageLayer.affineTransform = arrowImageTransform;
+ _copiedArrowImageLayer.position = arrowImagePosition;
+ _copiedArrowImageLayer.contents = (id)_arrowImageView.image.CGImage;
+ _copiedArrowImageLayer.contentsScale = _arrowImageView.image.scale;
+ }
+
+ [self layoutDefaultSubviews];
+}
+
+- (void)layoutDefaultSubviews {
+ self.contentView.frame = CGRectMake(
+ self.contentEdgeInsets.left + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight ? self.arrowSizeAuto.width : self.borderWidth),
+ self.contentEdgeInsets.top + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow ? self.arrowSizeAuto.height : self.borderWidth),
+ CGRectGetWidth(self.bounds) - self.borderWidth - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.arrowSpacingInHorizontal - (self.arrowSpacingInHorizontal > 0 ? 0 : self.borderWidth),
+ CGRectGetHeight(self.bounds) - self.borderWidth - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.arrowSpacingInVertical - (self.arrowSpacingInVertical > 0 ? 0 : self.borderWidth));
+ // 让点击响应区域与肉眼看到的圆角矩形保持一致,否则 contentView 内部的 subviews 就算要扩大点击区域也会受限制
+ self.contentView.qmui_outsideEdge = UIEdgeInsetsMake(MIN(0, -self.contentEdgeInsets.top), MIN(0, -self.contentEdgeInsets.left), MIN(0, -self.contentEdgeInsets.bottom), MIN(0, -self.contentEdgeInsets.right));
+ // contentView的圆角取一个比整个path的圆角小的最大值(极限情况下如果self.contentEdgeInsets.left比self.cornerRadius还大,那就意味着contentView不需要圆角了)
+ // 这么做是为了尽量去掉contentView对内容不必要的裁剪,以免有些东西被裁剪了看不到
+ CGFloat contentViewCornerRadius = fabs(MIN(CGRectGetMinX(self.contentView.frame) - self.cornerRadius, 0));
+ self.contentView.layer.cornerRadius = contentViewCornerRadius;
+
+ BOOL isImageViewShowing = [self isSubviewShowing:_imageView];
+ BOOL isTextLabelShowing = [self isSubviewShowing:_textLabel];
+ if (isImageViewShowing) {
+ [_imageView sizeToFit];
+ _imageView.frame = CGRectSetX(_imageView.frame, self.imageEdgeInsets.left);//, self.imageEdgeInsets.top + (self.contentMode == UIViewContentModeTop ? 0 : CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), CGRectGetHeight(_imageView.frame))));
+ if (self.contentMode == UIViewContentModeTop) {
+ _imageView.frame = CGRectSetY(_imageView.frame, self.imageEdgeInsets.top);
+ } else if (self.contentMode == UIViewContentModeBottom) {
+ _imageView.frame = CGRectSetY(_imageView.frame, CGRectGetHeight(self.contentView.bounds) - self.imageEdgeInsets.bottom - CGRectGetHeight(_imageView.frame));
+ } else {
+ _imageView.frame = CGRectSetY(_imageView.frame, self.imageEdgeInsets.top + CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds) - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets), CGRectGetHeight(_imageView.frame)));
+ }
+ }
+ if (isTextLabelShowing) {
+ CGFloat textLabelMinX = (isImageViewShowing ? ceil(CGRectGetMaxX(_imageView.frame) + self.imageEdgeInsets.right) : 0) + self.textEdgeInsets.left;
+ CGSize textLabelLimitSize = CGSizeMake(ceil(CGRectGetWidth(self.contentView.bounds) - textLabelMinX - self.textEdgeInsets.right), ceil(CGRectGetHeight(self.contentView.bounds) - UIEdgeInsetsGetVerticalValue(self.textEdgeInsets)));
+ CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize];
+ _textLabel.frame = CGRectMake(textLabelMinX, 0, textLabelLimitSize.width, ceil(textLabelSize.height));
+ if (self.contentMode == UIViewContentModeTop) {
+ _textLabel.frame = CGRectSetY(_textLabel.frame, self.textEdgeInsets.top);
+ } else if (self.contentMode == UIViewContentModeBottom) {
+ _textLabel.frame = CGRectSetY(_textLabel.frame, CGRectGetHeight(self.contentView.bounds) - self.textEdgeInsets.bottom - CGRectGetHeight(_textLabel.frame));
+ } else {
+ _textLabel.frame = CGRectSetY(_textLabel.frame, self.textEdgeInsets.top + CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds) - UIEdgeInsetsGetVerticalValue(self.textEdgeInsets), CGRectGetHeight(_textLabel.frame)));
+ }
+ }
+}
+
+- (UIBezierPath *)generatePathForBorder:(BOOL)forBorder {
+ BOOL isUsingArrowImage = !!self.arrowImage;
+ CGSize arrowSize = self.arrowSizeAuto;
+ CGFloat offset = forBorder ? self.borderWidth / 2.0 : self.borderWidth;
+ CGRect roundedRect = CGRectMake(offset + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight ? arrowSize.width - self.borderWidth : 0),
+ offset + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow ? arrowSize.height - self.borderWidth : 0),
+ CGRectGetWidth(self.bounds) - offset * 2 - self.arrowSpacingInHorizontal + (self.arrowSpacingInHorizontal > 0 ? self.borderWidth : 0),
+ CGRectGetHeight(self.bounds) - offset * 2 - self.arrowSpacingInVertical + (self.arrowSpacingInVertical > 0 ? self.borderWidth : 0));
+ CGFloat cornerRadius = forBorder ? self.cornerRadius : (self.cornerRadius - self.borderWidth / 2.0);
CGPoint leftTopArcCenter = CGPointMake(CGRectGetMinX(roundedRect) + cornerRadius, CGRectGetMinY(roundedRect) + cornerRadius);
CGPoint leftBottomArcCenter = CGPointMake(leftTopArcCenter.x, CGRectGetMaxY(roundedRect) - cornerRadius);
CGPoint rightTopArcCenter = CGPointMake(CGRectGetMaxX(roundedRect) - cornerRadius, leftTopArcCenter.y);
CGPoint rightBottomArcCenter = CGPointMake(rightTopArcCenter.x, leftBottomArcCenter.y);
+ // 从左上角逆时针绘制
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(leftTopArcCenter.x, CGRectGetMinY(roundedRect))];
[path addArcWithCenter:leftTopArcCenter radius:cornerRadius startAngle:M_PI * 1.5 endAngle:M_PI clockwise:NO];
+
+ if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight) {
+ // 箭头向左
+ if (!isUsingArrowImage) {
+ [path addLineToPoint:CGPointMake(CGRectGetMinX(roundedRect), _arrowMinY)];
+ [path addLineToPoint:CGPointMake(CGRectGetMinX(roundedRect) - arrowSize.width, _arrowMinY + arrowSize.height / 2)];
+ [path addLineToPoint:CGPointMake(CGRectGetMinX(roundedRect), _arrowMinY + arrowSize.height)];
+ }
+ }
+
[path addLineToPoint:CGPointMake(CGRectGetMinX(roundedRect), leftBottomArcCenter.y)];
[path addArcWithCenter:leftBottomArcCenter radius:cornerRadius startAngle:M_PI endAngle:M_PI * 0.5 clockwise:NO];
if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove) {
- // 让开,我要开始开始画三角形了,箭头向下
- [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMaxY(roundedRect))];
- [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMaxY(roundedRect) + arrowSize.height)];
- [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMaxY(roundedRect))];
+ // 箭头向下
+ if (!isUsingArrowImage) {
+ [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMaxY(roundedRect))];
+ [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMaxY(roundedRect) + arrowSize.height)];
+ [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMaxY(roundedRect))];
+ }
}
[path addLineToPoint:CGPointMake(rightBottomArcCenter.x, CGRectGetMaxY(roundedRect))];
[path addArcWithCenter:rightBottomArcCenter radius:cornerRadius startAngle:M_PI * 0.5 endAngle:0.0 clockwise:NO];
+
+ if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft) {
+ // 箭头向右
+ if (!isUsingArrowImage) {
+ [path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect), _arrowMinY + arrowSize.height)];
+ [path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect) + arrowSize.width, _arrowMinY + arrowSize.height / 2)];
+ [path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect), _arrowMinY)];
+ }
+ }
+
[path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect), rightTopArcCenter.y)];
[path addArcWithCenter:rightTopArcCenter radius:cornerRadius startAngle:0.0 endAngle:M_PI * 1.5 clockwise:NO];
if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) {
// 箭头向上
- [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMinY(roundedRect))];
- [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMinY(roundedRect) - arrowSize.height)];
- [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMinY(roundedRect))];
+ if (!isUsingArrowImage) {
+ [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMinY(roundedRect))];
+ [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMinY(roundedRect) - arrowSize.height)];
+ [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMinY(roundedRect))];
+ }
}
[path closePath];
-
- _backgroundLayer.path = path.CGPath;
- _backgroundLayer.shadowPath = path.CGPath;
- _backgroundLayer.frame = self.bounds;
-
- [self layoutDefaultSubviews];
-}
-
-- (void)layoutDefaultSubviews {
- self.contentView.frame = CGRectMake(self.borderWidth + self.contentEdgeInsets.left, (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove ? self.borderWidth : self.arrowSize.height + self.borderWidth) + self.contentEdgeInsets.top, CGRectGetWidth(self.bounds) - self.borderWidth * 2 - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), CGRectGetHeight(self.bounds) - self.arrowSize.height - self.borderWidth * 2 - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets));
- // contentView的圆角取一个比整个path的圆角小的最大值(极限情况下如果self.contentEdgeInsets.left比self.cornerRadius还大,那就意味着contentView不需要圆角了)
- // 这么做是为了尽量去掉contentView对内容不必要的裁剪,以免有些东西被裁剪了看不到
- CGFloat contentViewCornerRadius = fabs(MIN(CGRectGetMinX(self.contentView.frame) - self.cornerRadius, 0));
- self.contentView.layer.cornerRadius = contentViewCornerRadius;
-
- BOOL isImageViewShowing = [self isSubviewShowing:_imageView];
- BOOL isTextLabelShowing = [self isSubviewShowing:_textLabel];
- if (isImageViewShowing) {
- [_imageView sizeToFit];
- _imageView.frame = CGRectSetXY(_imageView.frame, self.imageEdgeInsets.left, CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), CGRectGetHeight(_imageView.frame) + self.imageEdgeInsets.top));
- }
- if (isTextLabelShowing) {
- CGFloat textLabelMinX = (isImageViewShowing ? ceil(CGRectGetMaxX(_imageView.frame) + self.imageEdgeInsets.right) : 0) + self.textEdgeInsets.left;
- CGSize textLabelLimitSize = CGSizeMake(ceil(CGRectGetWidth(self.contentView.bounds) - textLabelMinX), ceil(CGRectGetHeight(self.contentView.bounds) - self.textEdgeInsets.top - self.textEdgeInsets.bottom));
- CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize];
- CGPoint textLabelOrigin = CGPointMake(textLabelMinX, flat(CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), ceil(textLabelSize.height)) + self.textEdgeInsets.top));
- _textLabel.frame = CGRectMake(textLabelOrigin.x, textLabelOrigin.y, textLabelLimitSize.width, ceil(textLabelSize.height));
- }
+ return path;
}
- (void)setSourceBarItem:(__kindof UIBarItem *)sourceBarItem {
+ if (_sourceBarItem && _sourceBarItem != sourceBarItem) {
+ _sourceBarItem.qmui_viewLayoutDidChangeBlock = nil;
+ }
+
_sourceBarItem = sourceBarItem;
+ if (!_sourceBarItem) return;
+
__weak __typeof(self)weakSelf = self;
- if (!sourceBarItem.qmui_viewLayoutDidChangeBlock) {
- sourceBarItem.qmui_viewLayoutDidChangeBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) {
- if (!view.window || !weakSelf.superview) return;
- UIView *convertToView = weakSelf.popupWindow ? UIApplication.sharedApplication.delegate.window : weakSelf.superview;// 对于以 window 方式显示的情况,由于横竖屏旋转时,不同 window 的旋转顺序不同,所以可能导致 sourceBarItem 所在的 window 已经旋转了但 popupWindow 还没旋转(iOS 11 及以后),那么计算出来的坐标就错了,所以这里改为用 UIApplication window
- CGRect rect = [view qmui_convertRect:view.bounds toView:convertToView];
- weakSelf.sourceRect = rect;
- };
- }
+ // 每次都要重新定义 block,否则当不同的 popup 在同一个 sourceBarItem 显示,这个 block 内部得到的 weakSelf 可能是前一次的
+ sourceBarItem.qmui_viewLayoutDidChangeBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) {
+ if (!view.window || !weakSelf.superview) return;
+ UIView *convertToView = weakSelf.popupWindow ? UIApplication.sharedApplication.delegate.window : weakSelf.superview;// 对于以 window 方式显示的情况,由于横竖屏旋转时,不同 window 的旋转顺序不同,所以可能导致 sourceBarItem 所在的 window 已经旋转了但 popupWindow 还没旋转(iOS 11 及以后),那么计算出来的坐标就错了,所以这里改为用 UIApplication window
+ CGRect rect = [view qmui_convertRect:view.bounds toView:convertToView];
+ weakSelf.sourceRect = rect;
+ };
if (sourceBarItem.qmui_view && sourceBarItem.qmui_viewLayoutDidChangeBlock) {
sourceBarItem.qmui_viewLayoutDidChangeBlock(sourceBarItem, sourceBarItem.qmui_view);// update layout immediately
}
}
- (void)setSourceView:(__kindof UIView *)sourceView {
+ if (_sourceView && _sourceView != sourceView) {
+ _sourceView.qmui_frameDidChangeBlock = nil;
+ }
+
_sourceView = sourceView;
+ if (!_sourceView) return;
+
__weak __typeof(self)weakSelf = self;
sourceView.qmui_frameDidChangeBlock = ^(__kindof UIView * _Nonnull view, CGRect precedingFrame) {
if (!view.window || !weakSelf.superview) return;
@@ -248,6 +413,16 @@ - (void)setSourceRect:(CGRect)sourceRect {
}
}
+- (void)setNeedsUpdateLayout {
+ if (_shouldInvalidateLayout) return;
+ _shouldInvalidateLayout = YES;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (self->_shouldInvalidateLayout) {
+ [self updateLayout];
+ }
+ });
+}
+
- (void)updateLayout {
// call setter to layout immediately
if (self.sourceBarItem) {
@@ -257,6 +432,7 @@ - (void)updateLayout {
} else {
self.sourceRect = self.sourceRect;
}
+ _shouldInvalidateLayout = NO;
}
// 参数 targetRect 在 window 模式下是 window 的坐标系内的,如果是 subview 模式下则是 superview 坐标系内的
@@ -266,77 +442,195 @@ - (void)layoutWithTargetRect:(CGRect)targetRect {
return;
}
+ _currentLayoutDirection = self.preferLayoutDirection;
targetRect = self.popupWindow ? [self.popupWindow convertRect:targetRect toView:superview] : targetRect;
CGRect containerRect = superview.bounds;
- CGSize tipSize = [self sizeThatFits:CGSizeMake(self.maximumWidth, self.maximumHeight)];
- CGFloat preferredTipWidth = tipSize.width;
-
- // 保护tips最往左只能到达self.safetyMarginsOfSuperview.left
- CGFloat a = CGRectGetMidX(targetRect) - tipSize.width / 2;
- CGFloat tipMinX = MAX(CGRectGetMinX(containerRect) + self.safetyMarginsOfSuperview.left, a);
-
- CGFloat tipMaxX = tipMinX + tipSize.width;
- if (tipMaxX + self.safetyMarginsOfSuperview.right > CGRectGetMaxX(containerRect)) {
- // 右边超出了
- // 先尝试把右边超出的部分往左边挪,看是否会令左边到达临界点
- CGFloat distanceCanMoveToLeft = tipMaxX - (CGRectGetMaxX(containerRect) - self.safetyMarginsOfSuperview.right);
- if (tipMinX - distanceCanMoveToLeft >= CGRectGetMinX(containerRect) + self.safetyMarginsOfSuperview.left) {
- // 可以往左边挪
- tipMinX -= distanceCanMoveToLeft;
- } else {
- // 不可以往左边挪,那么让左边靠到临界点,然后再把宽度减小,以让右边处于临界点以内
- tipMinX = CGRectGetMinX(containerRect) + self.safetyMarginsOfSuperview.left;
- tipMaxX = CGRectGetMaxX(containerRect) - self.safetyMarginsOfSuperview.right;
- tipSize.width = MIN(tipSize.width, tipMaxX - tipMinX);
+ CGSize (^sizeToFitBlock)(void) = ^CGSize(void) {
+ CGSize result = CGSizeZero;
+ if (self.isVerticalLayoutDirection) {
+ result.width = CGRectGetWidth(containerRect) - UIEdgeInsetsGetHorizontalValue(self.safetyMarginsAvoidSafeAreaInsets);
+ } else if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft) {
+ result.width = CGRectGetMinX(targetRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.left;
+ } else if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight) {
+ result.width = CGRectGetWidth(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right - self.distanceBetweenSource - CGRectGetMaxX(targetRect);
}
- }
+ if (self.isHorizontalLayoutDirection) {
+ result.height = CGRectGetHeight(containerRect) - UIEdgeInsetsGetVerticalValue(self.safetyMarginsAvoidSafeAreaInsets);
+ } else if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove) {
+ result.height = CGRectGetMinY(targetRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.top;
+ } else if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) {
+ result.height = CGRectGetHeight(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom - self.distanceBetweenSource - CGRectGetMaxY(targetRect);
+ }
+ result = CGSizeMake(MIN(self.maximumWidth, result.width), MIN(self.maximumHeight, result.height));
+ return result;
+ };
- // 经过上面一番调整,可能tipSize.width发生变化,一旦宽度变化,高度要重新计算,所以重新调用一次sizeThatFits
- BOOL tipWidthChanged = tipSize.width != preferredTipWidth;
- if (tipWidthChanged) {
- tipSize = [self sizeThatFits:tipSize];
- }
- _currentLayoutDirection = self.preferLayoutDirection;
+ CGSize tipSize = [self sizeThatFits:sizeToFitBlock()];
+ CGFloat preferredTipWidth = tipSize.width;
+ CGFloat preferredTipHeight = tipSize.height;
+ CGFloat tipMinX = 0;
+ CGFloat tipMinY = 0;
- // 检查当前的最大高度是否超过任一方向的剩余空间,如果是,则强制减小最大高度,避免后面计算布局选择方向时死循环
- BOOL canShowAtAbove = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionAbove targetRect:targetRect tipSize:tipSize];
- BOOL canShowAtBelow = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionBelow targetRect:targetRect tipSize:tipSize];
-
- if (!canShowAtAbove && !canShowAtBelow) {
- // 上下都没有足够的空间,所以要调整maximumHeight
- CGFloat maximumHeightAbove = CGRectGetMinY(targetRect) - CGRectGetMinY(containerRect) - self.distanceBetweenSource - self.safetyMarginsOfSuperview.top;
- CGFloat maximumHeightBelow = CGRectGetMaxY(containerRect) - self.safetyMarginsOfSuperview.bottom - self.distanceBetweenSource - CGRectGetMaxY(targetRect);
- self.maximumHeight = MAX(self.minimumHeight, MAX(maximumHeightAbove, maximumHeightBelow));
- tipSize.height = self.maximumHeight;
- _currentLayoutDirection = maximumHeightAbove > maximumHeightBelow ? QMUIPopupContainerViewLayoutDirectionAbove : QMUIPopupContainerViewLayoutDirectionBelow;
+ if (self.isVerticalLayoutDirection) {
+ // 保护tips最往左只能到达self.safetyMarginsAvoidSafeAreaInsets.left
+ CGFloat a = 0;
+ switch (self.preferLayoutAlignment) {
+ case QMUIPopupContainerViewLayoutAlignmentLeading:
+ a = CGRectGetMinX(targetRect);
+ break;
+ case QMUIPopupContainerViewLayoutAlignmentTrailing:
+ a = CGRectGetMaxX(targetRect) - tipSize.width;
+ break;
+ default:
+ a = CGRectGetMidX(targetRect) - tipSize.width / 2;
+ break;
+ }
+ tipMinX = MAX(CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left, a);
- QMUILog(NSStringFromClass(self.class), @"%@, 因为上下都不够空间,所以最大高度被强制改为%@, 位于目标的%@", self, @(self.maximumHeight), maximumHeightAbove > maximumHeightBelow ? @"上方" : @"下方");
+ CGFloat tipMaxX = tipMinX + tipSize.width;
+ if (tipMaxX + self.safetyMarginsAvoidSafeAreaInsets.right > CGRectGetMaxX(containerRect)) {
+ // 右边超出了
+ // 先尝试把右边超出的部分往左边挪,看是否会令左边到达临界点
+ CGFloat distanceCanMoveToLeft = 0;
+ if (self.preferLayoutAlignment == QMUIPopupContainerViewLayoutAlignmentLeading && self.usesOppositeLayoutAlignmentIfNeeded) {
+ distanceCanMoveToLeft = tipMaxX - MIN(CGRectGetMaxX(targetRect), CGRectGetMaxX(containerRect) - self.safetyMarginsOfSuperview.right);// targetRect 可能溢出屏幕外,需要保护
+ if (tipMinX - distanceCanMoveToLeft >= CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left) {
+ // 可以往左边挪,走下面的统一逻辑
+ } else {
+ // 不可以往左边挪,那就算了按原始 alignment 来对待
+ distanceCanMoveToLeft = tipMaxX - (CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right);
+ }
+ } else {
+ distanceCanMoveToLeft = tipMaxX - (CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right);
+ }
+ if (tipMinX - distanceCanMoveToLeft >= CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left) {
+ // 可以往左边挪
+ tipMinX -= distanceCanMoveToLeft;
+ } else {
+ // 不可以往左边挪,那么让左边靠到临界点,然后再把宽度减小,以让右边处于临界点以内
+ tipMinX = CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left;
+ tipMaxX = CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right;
+ tipSize.width = MIN(tipSize.width, tipMaxX - tipMinX);
+ }
+ }
- } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove && !canShowAtAbove) {
- _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow;
- } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow && !canShowAtBelow) {
- _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionAbove;
- }
-
- CGFloat tipMinY = [self tipMinYWithTargetRect:targetRect tipSize:tipSize preferLayoutDirection:_currentLayoutDirection];
-
- // 当上下的剩余空间都比最小高度要小的时候,tip会靠在safetyMargins范围内的上(下)边缘
- if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove) {
- CGFloat tipMinYIfAlignSafetyMarginTop = CGRectGetMinY(containerRect) + self.safetyMarginsOfSuperview.top;
- tipMinY = MAX(tipMinY, tipMinYIfAlignSafetyMarginTop);
- } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) {
- CGFloat tipMinYIfAlignSafetyMarginBottom = CGRectGetMaxY(containerRect) - self.safetyMarginsOfSuperview.bottom - tipSize.height;
- tipMinY = MIN(tipMinY, tipMinYIfAlignSafetyMarginBottom);
+ // 经过上面一番调整,可能tipSize.width发生变化,一旦宽度变化,高度要重新计算,所以重新调用一次sizeThatFits
+ BOOL tipWidthChanged = tipSize.width != preferredTipWidth;
+ if (tipWidthChanged) {
+ tipSize = [self sizeThatFits:tipSize];
+ }
+
+ // 检查当前的最大高度是否超过任一方向的剩余空间,如果是,则强制减小最大高度,避免后面计算布局选择方向时死循环
+ BOOL canShowAtAbove = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionAbove targetRect:targetRect tipSize:tipSize];
+ BOOL canShowAtBelow = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionBelow targetRect:targetRect tipSize:tipSize];
+
+ if (!canShowAtAbove && !canShowAtBelow) {
+ // 上下都没有足够的空间,所以要调整maximumHeight
+ CGFloat maximumHeightAbove = CGRectGetMinY(targetRect) - CGRectGetMinY(containerRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.top;
+ CGFloat maximumHeightBelow = CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom - self.distanceBetweenSource - CGRectGetMaxY(targetRect);
+ self.maximumHeight = MAX(self.minimumHeight, MAX(maximumHeightAbove, maximumHeightBelow));
+ tipSize.height = self.maximumHeight;
+ _currentLayoutDirection = maximumHeightAbove > maximumHeightBelow ? QMUIPopupContainerViewLayoutDirectionAbove : QMUIPopupContainerViewLayoutDirectionBelow;
+
+ QMUILog(NSStringFromClass(self.class), @"%@, 因为上下都不够空间,所以最大高度被强制改为%@, 位于目标的%@", self, @(self.maximumHeight), maximumHeightAbove > maximumHeightBelow ? @"上方" : @"下方");
+
+ } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove && !canShowAtAbove) {
+ _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow;
+ tipSize.height = [self sizeThatFits:CGSizeMake(tipSize.width, sizeToFitBlock().height)].height;
+ } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow && !canShowAtBelow) {
+ _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionAbove;
+ tipSize.height = [self sizeThatFits:CGSizeMake(tipSize.width, sizeToFitBlock().height)].height;
+ }
+
+ tipMinY = [self tipOriginWithTargetRect:targetRect tipSize:tipSize preferLayoutDirection:_currentLayoutDirection].y;
+
+ // 当上下的剩余空间都比最小高度要小的时候,tip会靠在safetyMargins范围内的上(下)边缘
+ if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove) {
+ CGFloat tipMinYIfAlignSafetyMarginTop = CGRectGetMinY(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.top;
+ tipMinY = MAX(tipMinY, tipMinYIfAlignSafetyMarginTop);
+ } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) {
+ CGFloat tipMinYIfAlignSafetyMarginBottom = CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom - tipSize.height;
+ tipMinY = MIN(tipMinY, tipMinYIfAlignSafetyMarginBottom);
+ }
+
+ self.frame = CGRectFlatMake(tipMinX, tipMinY, tipSize.width, tipSize.height);
+
+ // 调整浮层里的箭头的位置
+ CGPoint targetRectCenter = CGPointGetCenterWithRect(targetRect);
+ CGFloat selfMidX = targetRectCenter.x - CGRectGetMinX(self.frame);
+ CGFloat arrowMinimumMinX = self.cornerRadius;
+ CGFloat arrowMaximumMinX = CGRectGetWidth(self.bounds) - self.cornerRadius - self.arrowSize.width;
+ _arrowMinX = MIN(arrowMaximumMinX, MAX(arrowMinimumMinX, selfMidX - self.arrowSizeAuto.width / 2));
+ } else {
+ // 保护tips最往上只能到达self.safetyMarginsAvoidSafeAreaInsets.top
+ CGFloat a = CGRectGetMidY(targetRect) - tipSize.height / 2;
+ tipMinY = MAX(CGRectGetMinY(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.top, a);
+
+ CGFloat tipMaxY = tipMinY + tipSize.height;
+ if (tipMaxY + self.safetyMarginsAvoidSafeAreaInsets.bottom > CGRectGetMaxY(containerRect)) {
+ // 下面超出了
+ // 先尝试把下面超出的部分往上面挪,看是否会令上面到达临界点
+ CGFloat distanceCanMoveToTop = tipMaxY - (CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom);
+ if (tipMinY - distanceCanMoveToTop >= CGRectGetMinY(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.top) {
+ // 可以往上面挪
+ tipMinY -= distanceCanMoveToTop;
+ } else {
+ // 不可以往上面挪,那么让上面靠到临界点,然后再把高度减小,以让下面处于临界点以内
+ tipMinY = CGRectGetMinY(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.top;
+ tipMaxY = CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom;
+ tipSize.height = MIN(tipSize.height, tipMaxY - tipMinY);
+ }
+ }
+
+ // 经过上面一番调整,可能tipSize.height发生变化,一旦高度变化,高度要重新计算,所以重新调用一次sizeThatFits
+ BOOL tipHeightChanged = tipSize.height != preferredTipHeight;
+ if (tipHeightChanged) {
+ tipSize = [self sizeThatFits:tipSize];
+ }
+
+ // 检查当前的最大宽度是否超过任一方向的剩余空间,如果是,则强制减小最大宽度,避免后面计算布局选择方向时死循环
+ BOOL canShowAtLeft = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionLeft targetRect:targetRect tipSize:tipSize];
+ BOOL canShowAtRight = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionRight targetRect:targetRect tipSize:tipSize];
+
+ if (!canShowAtLeft && !canShowAtRight) {
+ // 左右都没有足够的空间,所以要调整maximumWidth
+ CGFloat maximumWidthLeft = CGRectGetMinX(targetRect) - CGRectGetMinX(containerRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.left;
+ CGFloat maximumWidthRight = CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right - self.distanceBetweenSource - CGRectGetMaxX(targetRect);
+ self.maximumWidth = MAX(self.minimumWidth, MAX(maximumWidthLeft, maximumWidthRight));
+ tipSize.width = self.maximumWidth;
+ _currentLayoutDirection = maximumWidthLeft > maximumWidthRight ? QMUIPopupContainerViewLayoutDirectionLeft : QMUIPopupContainerViewLayoutDirectionRight;
+
+ QMUILog(NSStringFromClass(self.class), @"%@, 因为左右都不够空间,所以最大宽度被强制改为%@, 位于目标的%@", self, @(self.maximumWidth), maximumWidthLeft > maximumWidthRight ? @"左边" : @"右边");
+
+ } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft && !canShowAtLeft) {
+ _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionLeft;
+ tipSize.width = [self sizeThatFits:CGSizeMake(sizeToFitBlock().width, tipSize.height)].width;
+ } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow && !canShowAtRight) {
+ _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionRight;
+ tipSize.width = [self sizeThatFits:CGSizeMake(sizeToFitBlock().width, tipSize.height)].width;
+ }
+
+ tipMinX = [self tipOriginWithTargetRect:targetRect tipSize:tipSize preferLayoutDirection:_currentLayoutDirection].x;
+
+ // 当左右的剩余空间都比最小宽度要小的时候,tip会靠在safetyMargins范围内的左(右)边缘
+ if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft) {
+ CGFloat tipMinXIfAlignSafetyMarginLeft = CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left;
+ tipMinX = MAX(tipMinX, tipMinXIfAlignSafetyMarginLeft);
+ } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight) {
+ CGFloat tipMinXIfAlignSafetyMarginRight = CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right - tipSize.width;
+ tipMinX = MIN(tipMinX, tipMinXIfAlignSafetyMarginRight);
+ }
+
+ self.frame = CGRectFlatMake(tipMinX, tipMinY, tipSize.width, tipSize.height);
+
+ // 调整浮层里的箭头的位置
+ CGPoint targetRectCenter = CGPointGetCenterWithRect(targetRect);
+ CGFloat selfMidY = targetRectCenter.y - CGRectGetMinY(self.frame);
+ _arrowMinY = selfMidY - self.arrowSizeAuto.height / 2;
}
- self.frame = CGRectFlatMake(tipMinX, tipMinY, tipSize.width, tipSize.height);
-
- // 调整浮层里的箭头的位置
- CGPoint targetRectCenter = CGPointGetCenterWithRect(targetRect);
- CGFloat selfMidX = targetRectCenter.x - (CGRectGetMinX(containerRect) + CGRectGetMinX(self.frame));
- _arrowMinX = selfMidX - self.arrowSize.width / 2;
[self setNeedsLayout];
if (self.debug) {
@@ -348,24 +642,45 @@ - (void)layoutWithTargetRect:(CGRect)targetRect {
}
}
-- (CGFloat)tipMinYWithTargetRect:(CGRect)itemRect tipSize:(CGSize)tipSize preferLayoutDirection:(QMUIPopupContainerViewLayoutDirection)direction {
- CGFloat tipMinY = 0;
- if (direction == QMUIPopupContainerViewLayoutDirectionAbove) {
- tipMinY = CGRectGetMinY(itemRect) - tipSize.height - self.distanceBetweenSource;
- } else if (direction == QMUIPopupContainerViewLayoutDirectionBelow) {
- tipMinY = CGRectGetMaxY(itemRect) + self.distanceBetweenSource;
+- (CGPoint)tipOriginWithTargetRect:(CGRect)itemRect tipSize:(CGSize)tipSize preferLayoutDirection:(QMUIPopupContainerViewLayoutDirection)direction {
+ CGPoint tipOrigin = CGPointZero;
+ switch (direction) {
+ case QMUIPopupContainerViewLayoutDirectionAbove:
+ tipOrigin.y = CGRectGetMinY(itemRect) - tipSize.height - self.distanceBetweenSource;
+ break;
+ case QMUIPopupContainerViewLayoutDirectionBelow:
+ tipOrigin.y = CGRectGetMaxY(itemRect) + self.distanceBetweenSource;
+ break;
+ case QMUIPopupContainerViewLayoutDirectionLeft:
+ tipOrigin.x = CGRectGetMinX(itemRect) - tipSize.width - self.distanceBetweenSource;
+ break;
+ case QMUIPopupContainerViewLayoutDirectionRight:
+ tipOrigin.x = CGRectGetMaxX(itemRect) + self.distanceBetweenSource;
+ break;
+ default:
+ break;
}
- return tipMinY;
+ return tipOrigin;
}
- (BOOL)canTipShowAtSpecifiedLayoutDirect:(QMUIPopupContainerViewLayoutDirection)direction targetRect:(CGRect)itemRect tipSize:(CGSize)tipSize {
BOOL canShow = NO;
- CGFloat tipMinY = [self tipMinYWithTargetRect:itemRect tipSize:tipSize preferLayoutDirection:direction];
- if (direction == QMUIPopupContainerViewLayoutDirectionAbove) {
- canShow = tipMinY >= self.safetyMarginsOfSuperview.top;
- } else if (direction == QMUIPopupContainerViewLayoutDirectionBelow) {
- canShow = tipMinY + tipSize.height + self.safetyMarginsOfSuperview.bottom <= CGRectGetHeight(self.superview.bounds);
+ if (self.isVerticalLayoutDirection) {
+ CGFloat tipMinY = [self tipOriginWithTargetRect:itemRect tipSize:tipSize preferLayoutDirection:direction].y;
+ if (direction == QMUIPopupContainerViewLayoutDirectionAbove) {
+ canShow = tipMinY >= self.safetyMarginsAvoidSafeAreaInsets.top;
+ } else if (direction == QMUIPopupContainerViewLayoutDirectionBelow) {
+ canShow = tipMinY + tipSize.height + self.safetyMarginsAvoidSafeAreaInsets.bottom <= CGRectGetHeight(self.superview.bounds);
+ }
+ } else {
+ CGFloat tipMinX = [self tipOriginWithTargetRect:itemRect tipSize:tipSize preferLayoutDirection:direction].x;
+ if (direction == QMUIPopupContainerViewLayoutDirectionLeft) {
+ canShow = tipMinX >= self.safetyMarginsAvoidSafeAreaInsets.left;
+ } else if (direction == QMUIPopupContainerViewLayoutDirectionRight) {
+ canShow = tipMinX + tipSize.width + self.safetyMarginsAvoidSafeAreaInsets.right <= CGRectGetWidth(self.superview.bounds);
+ }
}
+
return canShow;
}
@@ -382,7 +697,7 @@ - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
QMUICommonViewController *viewController = (QMUICommonViewController *)self.popupWindow.rootViewController;
viewController.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations;
- self.previousKeyWindow = [UIApplication sharedApplication].keyWindow;
+ self.previousKeyWindow = UIApplication.sharedApplication.keyWindow;
[self.popupWindow makeKeyAndVisible];
isShowingByWindowMode = YES;
@@ -398,28 +713,43 @@ - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
if (animated) {
if (isShowingByWindowMode) {
- self.popupWindow.alpha = 0;
+ self.popupWindow.rootViewController.view.alpha = 0;// 请操作 vc.view.alpha 而不是 window.alpha,如果是后者,会导致 popup 显示出来前有一小段时间无法屏蔽界面的触摸事件,从而引发一些状态混乱问题
} else {
self.alpha = 0;
}
self.layer.transform = CATransform3DMakeScale(0.98, 0.98, 1);
- [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:12 options:UIViewAnimationOptionCurveLinear animations:^{
- self.layer.transform = CATransform3DMakeScale(1, 1, 1);
- } completion:^(BOOL finished) {
- if (completion) {
- completion(finished);
- }
- }];
- [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
- if (isShowingByWindowMode) {
- self.popupWindow.alpha = 1;
- } else {
- self.alpha = 1;
- }
- } completion:nil];
+ if (self.showingAnimationBlock) {
+ self.showingAnimationBlock(^{
+ self.layer.transform = CATransform3DMakeScale(1, 1, 1);
+ if (isShowingByWindowMode) {
+ self.popupWindow.rootViewController.view.alpha = 1;
+ } else {
+ self.alpha = 1;
+ }
+ }, ^(BOOL finished) {
+ if (completion) {
+ completion(finished);
+ }
+ }, isShowingByWindowMode, self.popupWindow.rootViewController.view, self);
+ } else {
+ [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:12 options:UIViewAnimationOptionCurveLinear animations:^{
+ self.layer.transform = CATransform3DMakeScale(1, 1, 1);
+ } completion:^(BOOL finished) {
+ if (completion) {
+ completion(finished);
+ }
+ }];
+ [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
+ if (isShowingByWindowMode) {
+ self.popupWindow.rootViewController.view.alpha = 1;
+ } else {
+ self.alpha = 1;
+ }
+ } completion:nil];
+ }
} else {
if (isShowingByWindowMode) {
- self.popupWindow.alpha = 1;
+ self.popupWindow.rootViewController.view.alpha = 1;
} else {
self.alpha = 1;
}
@@ -441,15 +771,25 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
BOOL isShowingByWindowMode = !!self.popupWindow;
if (animated) {
- [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
+ void (^a)(void) = ^void(void) {
if (isShowingByWindowMode) {
- self.popupWindow.alpha = 0;
+ self.popupWindow.rootViewController.view.alpha = 0;
} else {
self.alpha = 0;
}
- } completion:^(BOOL finished) {
+ };
+ void (^c)(BOOL finished) = ^void(BOOL finished) {
[self hideCompletionWithWindowMode:isShowingByWindowMode completion:completion];
- }];
+ };
+ if (self.hidingAnimationBlock) {
+ self.hidingAnimationBlock(a, c, isShowingByWindowMode, self.popupWindow.rootViewController.view, self);
+ } else {
+ [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
+ a();
+ } completion:^(BOOL finished) {
+ c(finished);
+ }];
+ }
} else {
[self hideCompletionWithWindowMode:isShowingByWindowMode completion:completion];
}
@@ -458,7 +798,7 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
- (void)hideCompletionWithWindowMode:(BOOL)windowMode completion:(void (^)(BOOL))completion {
if (windowMode) {
// 恢复 keyWindow 之前做一下检查,避免类似问题 https://github.com/Tencent/QMUI_iOS/issues/90
- if ([[UIApplication sharedApplication] keyWindow] == self.popupWindow) {
+ if (UIApplication.sharedApplication.keyWindow == self.popupWindow) {
[self.previousKeyWindow makeKeyWindow];
}
@@ -512,36 +852,126 @@ - (void)initPopupContainerViewWindowIfNeeded {
}
}
-/// 根据一个给定的大小,计算出符合这个大小的内容大小
+/// 根据一个给定的大小(包含箭头,不含 distanceBetweenSource ),计算出符合这个大小的内容大小(去掉箭头和白色内部的 contentEdgeInsets 后)
- (CGSize)contentSizeInSize:(CGSize)size {
- CGSize contentSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.borderWidth * 2, size.height - self.arrowSize.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.borderWidth * 2);
+ CGSize contentSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.borderWidth - self.arrowSpacingInHorizontal - (self.arrowSpacingInHorizontal > 0 ? 0 : self.borderWidth), size.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.borderWidth - self.arrowSpacingInVertical - (self.arrowSpacingInVertical > 0 ? 0 : self.borderWidth));
return contentSize;
}
/// 根据内容大小和外部限制的大小,计算出合适的self size(包含箭头)
- (CGSize)sizeWithContentSize:(CGSize)contentSize sizeThatFits:(CGSize)sizeThatFits {
- CGFloat resultWidth = contentSize.width + UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) + self.borderWidth * 2;
- resultWidth = MIN(resultWidth, sizeThatFits.width);// 宽度不能超过传进来的size.width
+ CGFloat resultWidth = contentSize.width + UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) + self.borderWidth + self.arrowSpacingInHorizontal + (self.arrowSpacingInHorizontal > 0 ? 0 : self.borderWidth);// 当存在箭头时,箭头会叠在 border 上,所以这里只加一条边
resultWidth = MAX(MIN(resultWidth, self.maximumWidth), self.minimumWidth);// 宽度必须在最小值和最大值之间
- resultWidth = ceil(resultWidth);
+ resultWidth = flat(resultWidth);
- CGFloat resultHeight = contentSize.height + UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) + self.arrowSize.height + self.borderWidth * 2;
- resultHeight = MIN(resultHeight, sizeThatFits.height);
+ CGFloat resultHeight = contentSize.height + UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) + self.borderWidth + self.arrowSpacingInVertical + (self.arrowSpacingInVertical > 0 ? 0 : self.borderWidth);// 当存在箭头时,箭头会叠在 border 上,所以这里只加一条边
resultHeight = MAX(MIN(resultHeight, self.maximumHeight), self.minimumHeight);
- resultHeight = ceil(resultHeight);
+ resultHeight = flat(resultHeight);
return CGSizeMake(resultWidth, resultHeight);
}
+- (BOOL)isHorizontalLayoutDirection {
+ return self.preferLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft || self.preferLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight;
+}
+
+- (BOOL)isVerticalLayoutDirection {
+ return self.preferLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove || self.preferLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow;
+}
+
+- (void)setArrowImage:(UIImage *)arrowImage {
+ _arrowImage = arrowImage;
+ if (arrowImage) {
+ _arrowSize = arrowImage.size;
+
+ if (!_arrowImageView) {
+ _arrowImageView = UIImageView.new;
+ _arrowImageView.tintColor = self.backgroundColor;
+ [self addSubview:_arrowImageView];
+ }
+ _arrowImageView.hidden = !!self.backgroundView;// 存在 backgroundView 时不要显示箭头(但依然要设置 _arrowImageView 的内容,以供 mask 用)
+ _arrowImageView.image = arrowImage;
+ _arrowImageView.bounds = CGRectMakeWithSize(arrowImage.size);
+ } else {
+ _arrowImageView.hidden = YES;
+ _arrowImageView.image = nil;
+ }
+}
+
+- (void)setArrowSize:(CGSize)arrowSize {
+ if (!self.arrowImage) {
+ _arrowSize = arrowSize;
+ if (self.isShowing) {
+ [self setNeedsUpdateLayout];
+ }
+ }
+}
+
+// self.arrowSize 规定的是上下箭头的宽高,如果 tip 布局在左右的话,arrowSize 的宽高则调转
+- (CGSize)arrowSizeAuto {
+ return self.isHorizontalLayoutDirection ? CGSizeMake(self.arrowSize.height, self.arrowSize.width) : self.arrowSize;
+}
+
+- (CGFloat)arrowSpacingInHorizontal {
+ return self.isHorizontalLayoutDirection ? self.arrowSizeAuto.width : 0;
+}
+
+- (CGFloat)arrowSpacingInVertical {
+ return self.isVerticalLayoutDirection ? self.arrowSizeAuto.height : 0;
+}
+
+- (void)setMinimumWidth:(CGFloat)minimumWidth {
+ _minimumWidth = minimumWidth;
+ if (self.isShowing) {
+ [self setNeedsUpdateLayout];
+ }
+}
+
+- (void)setMaximumWidth:(CGFloat)maximumWidth {
+ _maximumWidth = maximumWidth;
+ if (self.isShowing) {
+ [self setNeedsUpdateLayout];
+ }
+}
+
+- (void)setMinimumHeight:(CGFloat)minimumHeight {
+ _minimumHeight = minimumHeight;
+ if (self.isShowing) {
+ [self setNeedsUpdateLayout];
+ }
+}
+
+- (void)setMaximumHeight:(CGFloat)maximumHeight {
+ _maximumHeight = maximumHeight;
+ if (self.isShowing) {
+ [self setNeedsUpdateLayout];
+ }
+}
+
+- (UIEdgeInsets)safetyMarginsAvoidSafeAreaInsets {
+ UIEdgeInsets result = self.safetyMarginsOfSuperview;
+ if (self.isHorizontalLayoutDirection) {
+ result.left += self.superview.safeAreaInsets.left;
+ result.right += self.superview.safeAreaInsets.right;
+ } else {
+ result.top += self.superview.safeAreaInsets.top;
+ result.bottom += self.superview.safeAreaInsets.bottom;
+ }
+ return result;
+}
+
@end
@implementation QMUIPopupContainerView (UISubclassingHooks)
- (void)didInitialize {
+ _borderLayer = [CAShapeLayer layer];
+ [_borderLayer qmui_removeDefaultAnimations];
+ _borderLayer.fillColor = UIColor.clearColor.CGColor;
+ [self.layer addSublayer:_borderLayer];
+
_backgroundLayer = [CAShapeLayer layer];
- _backgroundLayer.shadowOffset = CGSizeMake(0, 2);
- _backgroundLayer.shadowOpacity = 1;
- _backgroundLayer.shadowRadius = 10;
+ [_backgroundLayer qmui_removeDefaultAnimations];
[self.layer addSublayer:_backgroundLayer];
_contentView = [[UIView alloc] init];
@@ -549,7 +979,7 @@ - (void)didInitialize {
[self addSubview:self.contentView];
// 由于浮层是在调用 showWithAnimated: 时才会被添加到 window 上,所以 appearance 也是在 showWithAnimated: 后才生效,这太晚了,会导致 showWithAnimated: 之前用到那些支持 appearance 的属性值都不准确,所以这里手动提前触发。
- [self updateAppearance];
+ [self qmui_applyAppearance];
}
- (CGSize)sizeThatFitsInContentView:(CGSize)size {
@@ -564,19 +994,17 @@ - (CGSize)sizeThatFitsInContentView:(CGSize)size {
BOOL isImageViewShowing = [self isSubviewShowing:_imageView];
if (isImageViewShowing) {
CGSize imageViewSize = [_imageView sizeThatFits:size];
- resultSize.width += ceil(imageViewSize.width) + self.imageEdgeInsets.left;
- resultSize.height += ceil(imageViewSize.height) + self.imageEdgeInsets.top;
+ resultSize.width += ceil(imageViewSize.width) + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets);
+ resultSize.height += ceil(imageViewSize.height) + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets);
}
BOOL isTextLabelShowing = [self isSubviewShowing:_textLabel];
if (isTextLabelShowing) {
- CGSize textLabelLimitSize = CGSizeMake(size.width - resultSize.width - self.imageEdgeInsets.right, size.height);
+ CGSize textLabelLimitSize = CGSizeMake(size.width - resultSize.width - UIEdgeInsetsGetHorizontalValue(self.textEdgeInsets), size.height);
CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize];
- resultSize.width += (isImageViewShowing ? self.imageEdgeInsets.right : 0) + ceil(textLabelSize.width) + self.textEdgeInsets.left;
- resultSize.height = MAX(resultSize.height, ceil(textLabelSize.height) + self.textEdgeInsets.top);
+ resultSize.width += ceil(textLabelSize.width) + UIEdgeInsetsGetHorizontalValue(self.textEdgeInsets);
+ resultSize.height = MAX(resultSize.height, ceil(textLabelSize.height) + UIEdgeInsetsGetVerticalValue(self.textEdgeInsets));
}
- resultSize.width = MIN(size.width, resultSize.width);
- resultSize.height = MIN(size.height, resultSize.height);
return resultSize;
}
@@ -600,12 +1028,13 @@ + (void)setDefaultAppearance {
appearance.maximumHeight = CGFLOAT_MAX;
appearance.minimumHeight = 0;
appearance.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionAbove;
+ appearance.usesOppositeLayoutAlignmentIfNeeded = YES;
appearance.distanceBetweenSource = 5;
appearance.safetyMarginsOfSuperview = UIEdgeInsetsMake(10, 10, 10, 10);
- appearance.backgroundColor = UIColorWhite;
+ appearance.backgroundColor = UIColorWhite;// 如果先设置了 UIView.appearance.backgroundColor,再使用最传统的 method_exchangeImplementations 交换 UIView.setBackgroundColor 方法,则会 crash。QMUI 这里是在 +initialize 时设置的,业务如果要 hook -[UIView setBackgroundColor:] 则需要比 +initialize 更早才行
appearance.maskViewBackgroundColor = UIColorMask;
appearance.highlightedBackgroundColor = nil;
- appearance.shadowColor = UIColorMakeWithRGBA(0, 0, 0, .1);
+ appearance.shadow = [NSShadow qmui_shadowWithColor:UIColorMakeWithRGBA(0, 0, 0, .1) shadowOffset:CGSizeMake(0, 2) shadowRadius:10];
appearance.borderColor = UIColorGrayLighten;
appearance.borderWidth = PixelOne;
appearance.cornerRadius = 10;
@@ -613,26 +1042,6 @@ + (void)setDefaultAppearance {
}
-- (void)updateAppearance {
- QMUIPopupContainerView *appearance = [QMUIPopupContainerView appearance];
- self.contentEdgeInsets = appearance.contentEdgeInsets;
- self.arrowSize = appearance.arrowSize;
- self.maximumWidth = appearance.maximumWidth;
- self.minimumWidth = appearance.minimumWidth;
- self.maximumHeight = appearance.maximumHeight;
- self.minimumHeight = appearance.minimumHeight;
- self.preferLayoutDirection = appearance.preferLayoutDirection;
- self.safetyMarginsOfSuperview = appearance.safetyMarginsOfSuperview;
- self.distanceBetweenSource = appearance.distanceBetweenSource;
- self.backgroundColor = appearance.backgroundColor;
- self.maskViewBackgroundColor = appearance.maskViewBackgroundColor;
- self.shadowColor = appearance.shadowColor;
- self.borderColor = appearance.borderColor;
- self.borderWidth = appearance.borderWidth;
- self.cornerRadius = appearance.cornerRadius;
- self.qmui_outsideEdge = appearance.qmui_outsideEdge;
-}
-
@end
@implementation QMUIPopContainerViewController
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.m
deleted file mode 100644
index 3244a543..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.m
+++ /dev/null
@@ -1,36 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUIPopupMenuBaseItem.m
-// QMUIKit
-//
-// Created by QMUI Team on 2018/8/21.
-//
-
-#import "QMUIPopupMenuBaseItem.h"
-
-@implementation QMUIPopupMenuBaseItem
-
-@synthesize title = _title;
-@synthesize height = _height;
-@synthesize handler = _handler;
-@synthesize menuView = _menuView;
-
-- (instancetype)init {
- if (self = [super init]) {
- self.height = -1;
- }
- return self;
-}
-
-- (void)updateAppearance {
-
-}
-
-@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h
deleted file mode 100644
index ee2b6019..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUIPopupMenuButtonItem.h
-// QMUIKit
-//
-// Created by QMUI Team on 2018/8/21.
-//
-
-#import "QMUIPopupMenuBaseItem.h"
-
-NS_ASSUME_NONNULL_BEGIN
-
-@class QMUIButton;
-
-/**
- * 配合 QMUIPopupMenuView 使用,用于可点击的菜单项。
- * 支持显示图片和标题,以及点击事件的回调。
- * 可在 QMUIPopupMenuView 里统一修改菜单项的样式,如果某个菜单项需要特殊调整,可通过 QMUIPopupMenuButtonItem.button 拿到 view 并进行调整。
- */
-@interface QMUIPopupMenuButtonItem : QMUIPopupMenuBaseItem
-
-/// item 里的图片,默认在左边,也可通过 item.button.imagePosition 修改图片的位置
-@property(nonatomic, strong, nullable) UIImage *image;
-
-/// 每个 item 都通过一个 button 来显示内容,可直接修改 button 的相关属性达到自定义的效果。
-@property(nonatomic, strong, readonly, nonnull) QMUIButton *button;
-
-/// item 被点击时的背景色,默认为 TableViewCellSelectedBackgroundColor,与列表的 cell 点击背景色一致。
-@property(nonatomic, strong, nullable) UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR;
-
-/// item 里图片和文字之间的间距,默认为 6,只有同时存在图片和文字时这个属性才会生效。
-@property(nonatomic, assign) CGFloat imageMarginRight UI_APPEARANCE_SELECTOR;
-
-/**
- 推荐的初始化方法
-
- @param image item 的图片
- @param title item 的文字
- @param handler item 点击时的事件回调,需要在这里自行隐藏 aMenuView
- @return item
- */
-+ (instancetype)itemWithImage:(nullable UIImage *)image title:(nullable NSString *)title handler:(nullable void (^)(QMUIPopupMenuButtonItem *aItem))handler;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m
deleted file mode 100644
index f650a00e..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m
+++ /dev/null
@@ -1,116 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUIPopupMenuButtonItem.m
-// QMUIKit
-//
-// Created by QMUI Team on 2018/8/21.
-//
-
-#import "QMUIPopupMenuButtonItem.h"
-#import "QMUIButton.h"
-#import "UIControl+QMUI.h"
-#import "QMUIPopupMenuView.h"
-#import "QMUICore.h"
-
-@interface QMUIPopupMenuButtonItem (UIAppearance)
-
-- (void)updateAppearanceForMenuButtonItem;
-@end
-
-@implementation QMUIPopupMenuButtonItem
-
-+ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title handler:(nullable void (^)(QMUIPopupMenuButtonItem *))handler {
- QMUIPopupMenuButtonItem *item = [[QMUIPopupMenuButtonItem alloc] init];
- item.image = image;
- item.title = title;
- item.handler = handler;
- return item;
-}
-
-- (instancetype)init {
- if (self = [super init]) {
- self.height = -1;
-
- _button = [[QMUIButton alloc] init];
- self.button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
- self.button.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES;
- [self.button addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
- [self addSubview:self.button];
-
- [self updateAppearanceForMenuButtonItem];
- }
- return self;
-}
-
-- (CGSize)sizeThatFits:(CGSize)size {
- return [self.button sizeThatFits:size];
-}
-
-- (void)layoutSubviews {
- [super layoutSubviews];
- self.button.frame = self.bounds;
-}
-
-- (void)setTitle:(NSString *)title {
- [super setTitle:title];
- [self.button setTitle:title forState:UIControlStateNormal];
-}
-
-- (void)setImage:(UIImage *)image {
- _image = image;
- [self.button setImage:image forState:UIControlStateNormal];
- if (image) {
- self.button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, self.imageMarginRight);
- } else {
- self.button.imageEdgeInsets = UIEdgeInsetsZero;
- }
-}
-
-- (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor {
- _highlightedBackgroundColor = highlightedBackgroundColor;
- self.button.highlightedBackgroundColor = highlightedBackgroundColor;
-}
-
-- (void)handleButtonEvent:(id)sender {
- if (self.handler) {
- self.handler(self);
- }
-}
-
-- (void)updateAppearance {
- self.button.titleLabel.font = self.menuView.itemTitleFont;
- [self.button setTitleColor:self.menuView.itemTitleColor forState:UIControlStateNormal];
- self.button.contentEdgeInsets = UIEdgeInsetsMake(0, self.menuView.padding.left, 0, self.menuView.padding.right);
-}
-
-@end
-
-@implementation QMUIPopupMenuButtonItem (UIAppearance)
-
-+ (void)initialize {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- [self setDefaultAppearanceForPopupMenuView];
- });
-}
-
-+ (void)setDefaultAppearanceForPopupMenuView {
- QMUIPopupMenuButtonItem *appearance = [QMUIPopupMenuButtonItem appearance];
- appearance.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor;
- appearance.imageMarginRight = 6;
-}
-
-- (void)updateAppearanceForMenuButtonItem {
- QMUIPopupMenuButtonItem *appearance = [QMUIPopupMenuButtonItem appearance];
- self.highlightedBackgroundColor = appearance.highlightedBackgroundColor;
- self.imageMarginRight = appearance.imageMarginRight;
-}
-
-@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.h
new file mode 100644
index 00000000..9e6e443f
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.h
@@ -0,0 +1,55 @@
+//
+// QMUIPopupMenuItem.h
+// QMUIKit
+//
+// Created by molice on 2024/6/17.
+// Copyright © 2024 QMUI Team. All rights reserved.
+//
+
+#import
+#import
+
+@class QMUIPopupMenuView;
+@class QMUIPopupMenuItemView;
+@protocol QMUIPopupMenuItemViewProtocol;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface QMUIPopupMenuItem : NSObject
+
+/// item 里的文字
+@property(nonatomic, copy, nullable) NSString *title;
+
+/// item 里的第二行文字
+@property(nonatomic, copy, nullable) NSString *subtitle;
+
+/// item 里的图片,默认会以 template 形式渲染,也即由 tintColor 决定颜色,可显式声明为 AlwaysOriginal 来以图片原本的颜色显示。
+@property(nonatomic, strong, nullable) UIImage *image;
+
+/// item 的高度,默认为 -1,-1 表示高度以 QMUIPopupMenuView.itemHeight 为准。如果设置为 QMUIViewSelfSizingHeight,则表示高度由 -[self sizeThatFits:] 返回的值决定。
+@property(nonatomic, assign) CGFloat height;
+
+/// 每次将 item 关联到 itemView 上时都会调用这个 block,可以理解为在 @c QMUIPopupMenuView.itemViewConfigurationHandler 之后立马会调用 @c QMUIPopupMenuItem.configurationBlock 。
+/// 业务可利用这个 block 做一些自定义的配置 itemView 的行为。
+@property(nonatomic, copy) void (^configurationBlock)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index);
+
+/// item 被点击时的事件处理接口
+/// @note 需要在内部自行隐藏 QMUIPopupMenuView。
+@property(nonatomic, copy, nullable) void (^handler)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index);
+
+/// 当前 item 所在的 QMUIPopupMenuView 的引用,只有在 item 被添加到菜单之后才有值。
+@property(nonatomic, weak, nullable) __kindof QMUIPopupMenuView *menuView;
+
++ (instancetype)itemWithTitle:(nullable NSString *)title
+ handler:(void (^ __nullable)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index))handler;
++ (instancetype)itemWithImage:(nullable UIImage *)image
+ title:(nullable NSString *)title
+ handler:(void (^ __nullable)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index))handler;
++ (instancetype)itemWithImage:(nullable UIImage *)image
+ title:(nullable NSString *)title
+ subtitle:(nullable NSString *)subtitle
+ handler:(void (^ __nullable)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index))handler;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.m
new file mode 100644
index 00000000..134def9d
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.m
@@ -0,0 +1,38 @@
+//
+// QMUIPopupMenuItem.m
+// QMUIKit
+//
+// Created by molice on 2024/6/17.
+// Copyright © 2024 QMUI Team. All rights reserved.
+//
+
+#import "QMUIPopupMenuItem.h"
+
+@implementation QMUIPopupMenuItem
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _height = -1;
+ }
+ return self;
+}
+
++ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title subtitle:(NSString *)subtitle handler:(void (^ _Nullable)(__kindof QMUIPopupMenuItem * _Nonnull, __kindof UIControl * _Nonnull, NSInteger, NSInteger))handler {
+ QMUIPopupMenuItem *item = [[self alloc] init];
+ item.image = image;
+ item.title = title;
+ item.subtitle = subtitle;
+ item.handler = handler;
+ return item;
+}
+
++ (instancetype)itemWithTitle:(NSString *)title handler:(void (^ _Nullable)(__kindof QMUIPopupMenuItem * _Nonnull, __kindof UIControl * _Nonnull, NSInteger, NSInteger))handler {
+ return [self itemWithImage:nil title:title subtitle:nil handler:handler];
+}
+
++ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title handler:(void (^ _Nullable)(__kindof QMUIPopupMenuItem * _Nonnull, __kindof UIControl * _Nonnull, NSInteger, NSInteger))handler {
+ return [self itemWithImage:image title:title subtitle:nil handler:handler];
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h
deleted file mode 100644
index 1e464a5f..00000000
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/*****
- * Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
- * http://opensource.org/licenses/MIT
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
-
-//
-// QMUIPopupMenuItemProtocol.h
-// QMUIKit
-//
-// Created by QMUI Team on 2018/8/21.
-//
-
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-
-@class QMUIPopupMenuView;
-
-@protocol QMUIPopupMenuItemProtocol
-
-/// item 里的文字
-@property(nonatomic, copy, nullable) NSString *title;
-
-/// item 的高度,默认为 -1。小于 0 的值均表示高度以 QMUIPopupMenuView.itemHeight 为准,当需要给某个 item 指定特定高度时才需要用这个 height 属性。
-@property(nonatomic, assign) CGFloat height;
-
-/// item 被点击时的事件处理,需要在内部自行隐藏 QMUIPopupMenuView。
-@property(nonatomic, copy, nullable) void (^handler)(__kindof NSObject *aItem);
-
-/// 当前 item 所在的 QMUIPopupMenuView 的引用,只有在 item 被添加到菜单之后才有值。
-@property(nonatomic, weak, nullable) QMUIPopupMenuView *menuView;
-
-/// item 被添加到 menuView 之后(也即 menuView 属性有值了)会被调用,可在这个方法里更新 item 的样式(因为某些样式可能需要从 menuView 那边读取)
-- (void)updateAppearance;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.h
new file mode 100644
index 00000000..ecf6da90
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.h
@@ -0,0 +1,34 @@
+//
+// QMUIPopupMenuItemView.h
+// QMUIKit
+//
+// Created by molice on 2024/6/17.
+// Copyright © 2024 QMUI Team. All rights reserved.
+//
+
+#import
+#import "QMUIPopupMenuItemViewProtocol.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class QMUIButton;
+@class QMUICheckbox;
+
+@interface QMUIPopupMenuItemView : UIControl
+
+/// 图片、文本、第二行文本所在的 view,不接受事件,点击事件由 self 接管。
+@property(nonatomic, strong, readonly) QMUIButton *button;
+
+/// 当菜单进入选择模式时,代表被选中的勾。非选择模式时不存在。
+@property(nonatomic, strong, readonly, nullable) UIImageView *checkmark;
+
+/// 当菜单进入选择模式时,代表被选中的圆形勾,不接受事件,勾选状态由菜单控制。非选择模式时不存在。
+@property(nonatomic, strong, readonly, nullable) QMUICheckbox *checkbox;
+
+@property(nonatomic, strong, nullable) UIColor *highlightedBackgroundColor;
+
+@property(nonatomic, assign) UIEdgeInsets padding;
+@property(nonatomic, assign) CGFloat spacingBetweenButtonAndCheck;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.m
new file mode 100644
index 00000000..878d9b43
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.m
@@ -0,0 +1,186 @@
+//
+// QMUIPopupMenuItemView.m
+// QMUIKit
+//
+// Created by molice on 2024/6/17.
+// Copyright © 2024 QMUI Team. All rights reserved.
+//
+
+#import "QMUIPopupMenuItemView.h"
+#import "QMUICore.h"
+#import "UIControl+QMUI.h"
+#import "QMUIPopupMenuView.h"
+#import "QMUILayouter.h"
+#import "QMUIButton.h"
+#import "QMUICheckbox.h"
+#import "UIView+QMUI.h"
+
+@interface QMUIPopupMenuItemView ()
+@property(nonatomic, assign) QMUIPopupMenuSelectedStyle selectedStyle;
+@property(nonatomic, assign) QMUIPopupMenuSelectedLayout selectedLayout;
+@end
+
+@implementation QMUIPopupMenuItemView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ _button = [[QMUIButton alloc] init];
+ _button.userInteractionEnabled = NO;
+ _button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeading;
+ _button.spacingBetweenImageAndTitle = 12;
+ _button.titleLabel.font = UIFontMake(16);
+ _button.subtitleLabel.font = UIFontMake(14);
+ _button.subtitleLabel.alpha = .6;
+ _button.adjustsTitleTintColorAutomatically = YES;
+ _button.tintColor = nil;// 跟随 superview
+ [self addSubview:_button];
+
+ _padding = UIEdgeInsetsMake(8, 0, 8, 0);
+ _spacingBetweenButtonAndCheck = 16;
+
+ if (QMUICMIActivated) {
+ self.highlightedBackgroundColor = TableViewGroupedCellSelectedBackgroundColor;
+ }
+ }
+ return self;
+}
+
+- (QMUILayouterItem *)generateLayouter {
+ QMUILayouterItem *button = [QMUILayouterItem itemWithView:self.button margin:UIEdgeInsetsZero grow:1 shrink:QMUILayouterShrinkDefault];
+ UIView *checkView = self.selectedStyle == QMUIPopupMenuSelectedStyleCheckmark ? self.checkmark : (self.selectedStyle == QMUIPopupMenuSelectedStyleCheckbox ? self.checkbox : nil);
+ QMUILayouterItem *check = checkView ? [QMUILayouterItem itemWithView:checkView margin:UIEdgeInsetsZero grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkNever] : nil;
+ check.visibleBlock = ^BOOL(QMUILayouterItem * _Nonnull aItem) {
+ return YES;// 不管 checkView 显示与否都一定占位,避免切换 selected 过程中内容宽度跳动
+ };
+ NSArray *items = nil;
+ if (check) {
+ if (self.selectedLayout == QMUIPopupMenuSelectedLayoutAtEnd) {
+ items = @[button, check];
+ } else if (self.selectedLayout == QMUIPopupMenuSelectedLayoutAtStart) {
+ items = @[check, button];
+ }
+ } else {
+ items = @[button];
+ }
+ QMUILayouterLinearHorizontal *h = [QMUILayouterLinearHorizontal itemWithChildItems:items spacingBetweenItems:_spacingBetweenButtonAndCheck horizontal:QMUILayouterAlignmentFill vertical:QMUILayouterAlignmentCenter];
+ h.margin = self.padding;
+ QMUILayouterLinearHorizontal *container = [QMUILayouterLinearHorizontal itemWithChildItems:@[h] spacingBetweenItems:0 horizontal:QMUILayouterAlignmentFill vertical:QMUILayouterAlignmentCenter];
+ return container;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+ return [[self generateLayouter] sizeThatFits:size];
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ QMUILayouterItem *l = [self generateLayouter];
+ l.frame = self.bounds;
+ [l layoutIfNeeded];
+}
+
+- (void)setEnabled:(BOOL)enabled {
+ [super setEnabled:enabled];
+ [self updateAlphaState];
+}
+
+- (void)setHighlighted:(BOOL)highlighted {
+ [super setHighlighted:highlighted];
+ [self updateAlphaState];
+}
+
+- (void)setSelected:(BOOL)selected {
+ [super setSelected:selected];
+ self.button.selected = selected;// 同步状态以使 button 上也可以感知到 selected
+ if (self.selectedStyle == QMUIPopupMenuSelectedStyleCheckmark) {
+ self.checkmark.hidden = !selected;
+ } else if (self.selectedStyle == QMUIPopupMenuSelectedStyleCheckbox) {
+ self.checkbox.hidden = NO;
+ self.checkbox.selected = selected;
+ } else {
+ self.checkmark.hidden = YES;
+ self.checkbox.hidden = YES;
+ self.checkbox.selected = NO;
+ }
+}
+
+- (void)setSelectedStyle:(QMUIPopupMenuSelectedStyle)selectedStyle {
+ _selectedStyle = selectedStyle;
+ if (selectedStyle == QMUIPopupMenuSelectedStyleCheckmark) {
+ if (!_checkmark) {
+ _checkmark = [[UIImageView alloc] initWithImage:[TableViewCellCheckmarkImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
+ [self addSubview:_checkmark];
+ }
+ _checkmark.hidden = !self.selected;
+ _checkbox.hidden = YES;
+ } else if (selectedStyle == QMUIPopupMenuSelectedStyleCheckbox) {
+ if (!_checkbox) {
+ _checkbox = QMUICheckbox.new;
+ _checkbox.tintColor = nil;
+ _checkbox.userInteractionEnabled = NO;
+ [self addSubview:_checkbox];
+ }
+ _checkbox.hidden = NO;
+ _checkbox.selected = self.selected;
+ _checkmark.hidden = YES;
+ } else {
+ _checkmark.hidden = YES;
+ _checkbox.hidden = YES;
+ }
+ [self setNeedsLayout];
+}
+
+- (void)setSelectedLayout:(QMUIPopupMenuSelectedLayout)selectedLayout {
+ _selectedLayout = selectedLayout;
+ [self setNeedsLayout];
+}
+
+- (void)updateAlphaState {
+ if (!self.enabled) {
+ [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ obj.alpha = UIControlDisabledAlpha;
+ }];
+ if (self.highlightedBackgroundColor) {
+ self.backgroundColor = nil;
+ }
+ return;
+ }
+
+ [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+ obj.alpha = 1;
+ }];
+ if (self.highlighted) {
+ if (self.highlightedBackgroundColor) {
+ self.backgroundColor = self.highlightedBackgroundColor;
+ }
+ return;
+ }
+ if (self.highlightedBackgroundColor) {
+ self.backgroundColor = nil;
+ }
+}
+
+#pragma mark -
+
+@synthesize item = _item;
+- (void)setItem:(__kindof QMUIPopupMenuItem *)item {
+ _item = item;
+ [self.button setImage:item.image.renderingMode == UIImageRenderingModeAutomatic ? [item.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] : item.image forState:UIControlStateNormal];
+ [self.button setTitle:item.title forState:UIControlStateNormal];
+ self.button.subtitle = item.subtitle;
+
+ QMUIPopupMenuView *menu = item.menuView;
+
+ self.padding = UIEdgeInsetsMake(self.padding.top, menu.padding.left, self.padding.bottom, menu.padding.right);
+
+ if (menu.allowsSelection) {
+ self.selectedStyle = menu.selectedStyle;
+ self.selectedLayout = menu.selectedLayout;
+ } else {
+ self.selectedStyle = (QMUIPopupMenuSelectedStyle)-1;// 表示清空
+ }
+
+ [self setNeedsLayout];
+}
+
+@end
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemViewProtocol.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemViewProtocol.h
new file mode 100644
index 00000000..18686536
--- /dev/null
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemViewProtocol.h
@@ -0,0 +1,23 @@
+//
+// QMUIPopupMenuItemViewProtocol.h
+// QMUIKit
+//
+// Created by molice on 2024/6/17.
+// Copyright © 2024 QMUI Team. All rights reserved.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class QMUIPopupMenuItem;
+
+@protocol QMUIPopupMenuItemViewProtocol
+
+@required
+
+/// 当前 itemView 关联的 item,在 cellForRow 时会被设置。itemView 内所有与 item 强相关的内容均应在 setItem: 方法里设置。
+@property(nonatomic, weak, nullable) __kindof QMUIPopupMenuItem *item;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h
index 9dd7785e..94538d7d 100644
--- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h
+++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h
@@ -1,10 +1,10 @@
-/*****
+/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
- * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved.
+ * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *****/
+ */
//
// QMUIPopupMenuView.h
@@ -15,26 +15,47 @@
#import