Skip to content

Commit c88b9c3

Browse files
committed
Merge commit '03cf6522880329aa624ffb84d7493d20b82c0f61'
2 parents 762b89e + 03cf652 commit c88b9c3

11 files changed

Lines changed: 246 additions & 47 deletions

File tree

QMUI/QMUIKit/UIComponents/QMUIZoomImageView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
/**
3838
* 比如常见的上传头像预览界面中间有一个用于裁剪的方框,则 viewportRect 必须被设置为这个方框在 zoomImageView 坐标系内的 frame,否则拖拽图片时无法正确限制图片的显示范围
39+
* @note 图片的初始位置会位于 viewportRect 正中间
3940
* @note 如果想要图片覆盖整个 viewportRect,将 contentMode 设置为 UIViewContentModeScaleAspectFill 即可
4041
* 如果设置为 CGRectZero 则表示使用默认值,默认值为和整个 zoomImageView 一样大
4142
*/

QMUI/QMUIKit/UIComponents/QMUIZoomImageView.m

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,22 @@ - (void)revertZooming {
204204
if (shouldFireDidZoomingManual) {
205205
[self handleDidEndZooming];
206206
}
207+
208+
// 当图片比 viewport 的区域更大时,要把图片放在 viewport 正中间
209+
self.scrollView.contentOffset = ({
210+
CGFloat x = self.scrollView.contentOffset.x;
211+
CGFloat y = self.scrollView.contentOffset.y;
212+
CGRect viewport = [self finalViewportRect];
213+
if (!CGRectIsEmpty(viewport)) {
214+
if (CGRectGetWidth(viewport) < CGRectGetWidth(self.imageView.frame)) {
215+
x = (CGRectGetWidth(self.imageView.frame) / 2 - CGRectGetWidth(viewport) / 2) - CGRectGetMinX(viewport);
216+
}
217+
if (CGRectGetHeight(viewport) < CGRectGetHeight(self.imageView.frame)) {
218+
y = (CGRectGetHeight(self.imageView.frame) / 2 - CGRectGetHeight(viewport) / 2) - CGRectGetMinY(viewport);
219+
}
220+
}
221+
CGPointMake(x, y);
222+
});
207223
}
208224

209225
- (void)setZoomScale:(CGFloat)zoomScale animated:(BOOL)animated {
@@ -354,7 +370,7 @@ - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
354370

355371
- (CGRect)finalViewportRect {
356372
CGRect rect = self.viewportRect;
357-
if (CGRectIsEmpty(rect)) {
373+
if (CGRectIsEmpty(rect) && !CGRectIsEmpty(self.bounds)) {
358374
// 有可能此时还没有走到过 layoutSubviews 因此拿不到正确的 scrollView 的 size,因此这里要强制 layout 一下
359375
if (!CGSizeEqualToSize(self.scrollView.bounds.size, self.bounds.size)) {
360376
[self setNeedsLayout];

QMUI/QMUIKit/UIKitExtensions/QMUIButton.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,17 @@ typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) {
5151
QMUINavigationButtonPositionRight, // 用于rightBarButtonItem,如果用于rightBarButtonItems,则只对最右边的item使用,其他item使用QMUINavigationButtonPositionNone
5252
};
5353

54-
5554
/**
5655
* 提供以下功能:
5756
* <ol>
5857
* <li>highlighted、disabled状态均通过改变整个按钮的alpha来表现,无需分别设置不同state下的titleColor、image</li>
5958
* <li>支持点击时改变背景色颜色(<i>highlightedBackgroundColor</i>)</li>
6059
* <li>支持点击时改变边框颜色(<i>highlightedBorderColor</i>)</li>
6160
* <li>支持设置图片在按钮内的位置,无需自行调整imageEdgeInsets(<i>imagePosition</i>)</li>
61+
* <li>支持设置按钮内文本和图片之间的间距,无需自行调整titleEdgeInests、imageEdgeInsets、contentEdgeInsets(<i>spacingBetweenImageAndTitle</i>)</li>
6262
* </ol>
6363
*/
64+
6465
@interface QMUIButton : UIButton
6566

6667
/**
@@ -107,6 +108,13 @@ typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) {
107108
*/
108109
@property(nonatomic, assign) QMUIButtonImagePosition imagePosition;
109110

111+
/**
112+
* 设置按钮里图标和文字之间的间隔,会自动响应 imagePosition 的变化而变化,默认为0。<br/>
113+
* 系统默认实现需要同时设置 titleEdgeInsets 和 imageEdgeInsets,同时还需考虑 contentEdgeInsets 的增加(否则不会影响布局,可能会让图标或文字溢出或挤压),使用该属性可以避免以上情况。<br/>
114+
* @warning 会与 imageEdgeInsets、 imageEdgeInsets、 contentEdgeInsets 共同作用。
115+
*/
116+
@property(nonatomic, assign) CGFloat spacingBetweenImageAndTitle;
117+
110118
@end
111119

112120

QMUI/QMUIKit/UIKitExtensions/QMUIButton.m

Lines changed: 133 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ @interface QMUIButton ()
1919

2020
@property(nonatomic, strong) CALayer *highlightedBackgroundLayer;
2121
@property(nonatomic, strong) UIColor *originBorderColor;
22+
23+
// 记录上一次的 spacing 和 lastImagePosition,用于恢复状态
24+
@property(nonatomic, assign) CGFloat lastSpacingBetweenImageAndTitle;
25+
@property(nonatomic, assign) QMUIButtonImagePosition lastImagePosition;
26+
@property(nonatomic, assign) BOOL hasInitialSpacingBetweenImageAndTitle;
27+
2228
- (void)didInitialized;// UISubclassingHooks
29+
2330
@end
2431

2532
@implementation QMUIButton
@@ -58,6 +65,7 @@ - (void)didInitialized {
5865

5966
// 图片默认在按钮左边,与系统UIButton保持一致
6067
self.imagePosition = QMUIButtonImagePositionLeft;
68+
self.lastImagePosition = QMUIButtonImagePositionLeft;
6169
}
6270

6371
- (CGSize)sizeThatFits:(CGSize)size {
@@ -274,9 +282,130 @@ - (void)layoutSubviews {
274282
}
275283
}
276284

285+
- (UIEdgeInsets)adjustTitleInsetsWithSpacingValue:(CGFloat)spacingValue imagePositionValue:(QMUIButtonImagePosition)imagePositionValue insetsValue:(UIEdgeInsets)insetsValue {
286+
287+
switch (imagePositionValue) {
288+
case QMUIButtonImagePositionTop:
289+
return UIEdgeInsetsMake(insetsValue.top + spacingValue, insetsValue.left, insetsValue.bottom - spacingValue, insetsValue.right);
290+
break;
291+
292+
case QMUIButtonImagePositionBottom:
293+
return UIEdgeInsetsMake(insetsValue.top - spacingValue, insetsValue.left, insetsValue.bottom + spacingValue, insetsValue.right);
294+
break;
295+
296+
case QMUIButtonImagePositionLeft:
297+
return UIEdgeInsetsMake(insetsValue.top, insetsValue.left + spacingValue, insetsValue.bottom, insetsValue.right - spacingValue);
298+
break;
299+
300+
case QMUIButtonImagePositionRight:
301+
return UIEdgeInsetsMake(insetsValue.top, insetsValue.left - spacingValue, insetsValue.bottom, insetsValue.right + spacingValue);
302+
break;
303+
}
304+
}
305+
306+
- (UIEdgeInsets)adjustImageInsetsWithSpacingValue:(CGFloat)spacingValue imagePositionValue:(QMUIButtonImagePosition)imagePositionValue insetsValue:(UIEdgeInsets)insetsValue {
307+
308+
switch (imagePositionValue) {
309+
case QMUIButtonImagePositionTop:
310+
return UIEdgeInsetsMake(insetsValue.top - spacingValue, insetsValue.left, insetsValue.bottom + spacingValue, insetsValue.right);
311+
break;
312+
313+
case QMUIButtonImagePositionBottom:
314+
return UIEdgeInsetsMake(insetsValue.top + spacingValue, insetsValue.left, insetsValue.bottom - spacingValue, insetsValue.right);
315+
break;
316+
317+
case QMUIButtonImagePositionLeft:
318+
return UIEdgeInsetsMake(insetsValue.top, insetsValue.left - spacingValue, insetsValue.bottom, insetsValue.right + spacingValue);
319+
break;
320+
321+
case QMUIButtonImagePositionRight:
322+
return UIEdgeInsetsMake(insetsValue.top, insetsValue.left + spacingValue, insetsValue.bottom, insetsValue.right - spacingValue);
323+
break;
324+
}
325+
}
326+
327+
- (UIEdgeInsets)adjustContentInsetsWithSpacingValue:(CGFloat)spacingValue imagePositionValue:(QMUIButtonImagePosition)imagePositionValue insetsValue:(UIEdgeInsets)insetsValue {
328+
329+
switch (imagePositionValue) {
330+
case QMUIButtonImagePositionTop:
331+
case QMUIButtonImagePositionBottom:
332+
return UIEdgeInsetsMake(insetsValue.top + spacingValue, insetsValue.left, insetsValue.bottom + spacingValue, insetsValue.right);
333+
break;
334+
335+
case QMUIButtonImagePositionLeft:
336+
case QMUIButtonImagePositionRight:
337+
return UIEdgeInsetsMake(insetsValue.top, insetsValue.left + spacingValue, insetsValue.bottom, insetsValue.right + spacingValue);
338+
break;
339+
}
340+
}
341+
342+
- (void)adjustInsetsWithSpacingValue:(CGFloat)spacingValue imagePositionValue:(QMUIButtonImagePosition)imagePositionValue {
343+
UIEdgeInsets titleEdgeInsets = [self adjustTitleInsetsWithSpacingValue:spacingValue imagePositionValue:imagePositionValue insetsValue:self.titleEdgeInsets];
344+
UIEdgeInsets imageEdgeInsets = [self adjustImageInsetsWithSpacingValue:spacingValue imagePositionValue:imagePositionValue insetsValue:self.imageEdgeInsets];
345+
UIEdgeInsets contentEdgeInsets = [self adjustContentInsetsWithSpacingValue:spacingValue imagePositionValue:imagePositionValue insetsValue:self.contentEdgeInsets];
346+
347+
[super setTitleEdgeInsets:titleEdgeInsets];
348+
[super setImageEdgeInsets:imageEdgeInsets];
349+
[super setContentEdgeInsets:contentEdgeInsets];
350+
}
351+
352+
// 恢复到上一次的 insets 设置
353+
- (void)restoreInsets {
354+
CGFloat spacingValue = -self.lastSpacingBetweenImageAndTitle / 2;
355+
356+
[self adjustInsetsWithSpacingValue:spacingValue imagePositionValue:self.lastImagePosition];
357+
}
358+
359+
// 重新保存当前 insets
360+
- (void)saveInsets {
361+
CGFloat spacingValue = self.spacingBetweenImageAndTitle / 2;
362+
363+
[self adjustInsetsWithSpacingValue:spacingValue imagePositionValue:self.imagePosition];
364+
}
365+
366+
// 检查 insets 的设置,每次都恢复上一次没设置前的状态重新设置,避免多次赋值的问题。
367+
- (void)checkInsetsChange {
368+
369+
// 设置过则需要回滚到未设置的状态
370+
if (self.hasInitialSpacingBetweenImageAndTitle) {
371+
[self restoreInsets];
372+
}
373+
374+
[self saveInsets];
375+
376+
self.lastImagePosition = self.imagePosition;
377+
378+
[self setNeedsLayout];
379+
}
380+
381+
- (void)setSpacingBetweenImageAndTitle:(CGFloat)spacingBetweenImageAndTitle {
382+
_spacingBetweenImageAndTitle = spacingBetweenImageAndTitle;
383+
384+
[self checkInsetsChange];
385+
386+
self.hasInitialSpacingBetweenImageAndTitle = YES;
387+
self.lastSpacingBetweenImageAndTitle = self.spacingBetweenImageAndTitle;
388+
}
389+
277390
- (void)setImagePosition:(QMUIButtonImagePosition)imagePosition {
278391
_imagePosition = imagePosition;
279-
[self setNeedsLayout];
392+
393+
[self checkInsetsChange];
394+
}
395+
396+
- (void)setTitleEdgeInsets:(UIEdgeInsets)titleEdgeInsets {
397+
UIEdgeInsets adjustedEdgeInsets = [self adjustTitleInsetsWithSpacingValue:self.spacingBetweenImageAndTitle / 2 imagePositionValue:self.imagePosition insetsValue:titleEdgeInsets];
398+
[super setTitleEdgeInsets:adjustedEdgeInsets];
399+
}
400+
401+
- (void)setImageEdgeInsets:(UIEdgeInsets)imageEdgeInsets {
402+
UIEdgeInsets adjustedEdgeInsets = [self adjustImageInsetsWithSpacingValue:self.spacingBetweenImageAndTitle / 2 imagePositionValue:self.imagePosition insetsValue:imageEdgeInsets];
403+
[super setImageEdgeInsets:adjustedEdgeInsets];
404+
}
405+
406+
- (void)setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets {
407+
UIEdgeInsets adjustedEdgeInsets = [self adjustContentInsetsWithSpacingValue:self.spacingBetweenImageAndTitle / 2 imagePositionValue:self.imagePosition insetsValue:contentEdgeInsets];
408+
[super setContentEdgeInsets:adjustedEdgeInsets];
280409
}
281410

282411
- (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor {
@@ -572,9 +701,9 @@ + (void)renderNavigationButtonAppearanceStyle {
572701
if (!CGSizeEqualToSize(customBackIndicatorImageSize, systemBackIndicatorImageSize)) {
573702
CGFloat imageExtensionVerticalFloat = CGFloatGetCenter(systemBackIndicatorImageSize.height, customBackIndicatorImageSize.height);
574703
customBackIndicatorImage = [customBackIndicatorImage qmui_imageWithSpacingExtensionInsets:UIEdgeInsetsMake(imageExtensionVerticalFloat,
575-
0,
576-
imageExtensionVerticalFloat,
577-
systemBackIndicatorImageSize.width - customBackIndicatorImageSize.width)];
704+
0,
705+
imageExtensionVerticalFloat,
706+
systemBackIndicatorImageSize.width - customBackIndicatorImageSize.width)];
578707
}
579708

580709
navBarAppearance.backIndicatorImage = customBackIndicatorImage;

QMUI/QMUIKit/UIKitExtensions/QMUISearchController.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ - (void)dealloc {
193193
self.searchDisplayController.delegate = nil;
194194
}
195195

196+
- (void)viewDidLoad {
197+
[super viewDidLoad];
198+
// 主动触发 loadView,如果不这么做,那么有可能直到 QMUISearchController 被销毁,这期间 self.searchController 都没有被触发 loadView,然后在 dealloc 时就会报错,提示尝试在释放 self.searchController 时触发了 self.searchController 的 loadView
199+
[self.searchController loadViewIfNeeded];
200+
}
201+
196202
- (void)setSearchResultsDelegate:(id<QMUISearchControllerDelegate>)searchResultsDelegate {
197203
_searchResultsDelegate = searchResultsDelegate;
198204

QMUI/QMUIKit/UIKitExtensions/QMUITextView.m

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
@interface QMUITextView ()
2323

2424
@property(nonatomic, assign) BOOL debug;
25-
@property(nonatomic, assign) BOOL textChangedBecauseOfPaste; // 标志本次触发对handleTextChange:的调用,是否因为粘贴
25+
@property(nonatomic, assign) BOOL shouldRejectSystemScroll;// 如果在 handleTextChanged: 里主动调整 contentOffset,则为了避免被系统的自动调整覆盖,会利用这个标记去屏蔽系统对 setContentOffset: 的调用
2626
@property(nonatomic, assign) BOOL callingSizeThatFitsByAutoResizable; // 标志本次调用 sizeThatFits: 是因为 handleTextChange: 里计算高度导致的
2727

2828
@property(nonatomic, strong) UILabel *placeholderLabel;
@@ -217,7 +217,7 @@ - (void)handleTextChanged:(id)sender {
217217
// 计算高度
218218
if (self.autoResizable) {
219219

220-
// 注意,这里 iOS 8 及以下有兼容问题,请查看文件里的 sizeThatFits:
220+
// 注意,这里 iOS 10 以下有兼容问题,请查看文件里的 sizeThatFits:
221221
self.callingSizeThatFitsByAutoResizable = YES;
222222
CGFloat resultHeight = [textView sizeThatFits:CGSizeMake(CGRectGetWidth(self.bounds), CGFLOAT_MAX)].height;
223223
self.callingSizeThatFitsByAutoResizable = NO;
@@ -231,27 +231,28 @@ - (void)handleTextChanged:(id)sender {
231231
}
232232
}
233233

234-
// iOS7的textView在内容可滚动的情况下,最后一行输入时文字会跑到可视区域外,因此要修复一下
235-
// 由于我们在文字换行的瞬间更改了输入框高度,所以即便内容不可滚动,换行瞬间contentOffset也是错的,所以这里完全接管了对contentOffset的自动调整
234+
// 系统的 UITextView 在文字可滚动的情况下在最后一行输入,文字是贴边的(并不会考虑 textContainerInset.bottom),所以这里接管了 UITextView 的文字滚动
236235
CGRect caretRect = [textView caretRectForPosition:textView.selectedTextRange.end];
237-
if (self.debug) NSLog(@"调整前,caretRect.maxY = %f, contentOffset.y = %f, bounds.height = %f", CGRectGetMaxY(caretRect), textView.contentOffset.y, CGRectGetHeight(textView.bounds));
236+
if (self.debug) NSLog(@"调整前,caretRect.maxY = %.2f, contentOffset.y = %.2f, contentSize.height = %.2f bounds.height = %.2f", CGRectGetMaxY(caretRect), textView.contentOffset.y, textView.contentSize.height, CGRectGetHeight(textView.bounds));
238237

239238
CGFloat caretMarginBottom = self.textContainerInset.bottom;
240-
if (ceil(CGRectGetMaxY(caretRect) + caretMarginBottom) >= textView.contentOffset.y + CGRectGetHeight(textView.bounds)) {
241-
CGFloat contentOffsetY = MAX(0, CGRectGetMaxY(caretRect) + caretMarginBottom - CGRectGetHeight(textView.bounds));
242-
if (self.debug) NSLog(@"调整后,contentOffset.y = %f", contentOffsetY);
239+
if (CGRectGetMaxY(caretRect) + caretMarginBottom >= textView.contentOffset.y + CGRectGetHeight(textView.bounds)) {
240+
CGFloat contentOffsetY = fmax(0, ceil(CGRectGetMaxY(caretRect) + caretMarginBottom - CGRectGetHeight(textView.bounds)));
241+
if (self.debug) NSLog(@"调整后,contentOffset.y = %.2f, contentSize.height = %.2f", contentOffsetY, textView.contentSize.height);
243242

244-
// 如果是粘贴导致光标掉出可视区域,则用动画去调整它(如果不用动画会不准,因为此时contentSize还是错的)
245-
// 如果是普通的键入换行导致光标掉出可视区域,则不用动画,否则会跳来跳去,但这会带来的问题就是换行没动画,不优雅😂
246-
[textView setContentOffset:CGPointMake(textView.contentOffset.x, contentOffsetY) animated:self.textChangedBecauseOfPaste ? YES : NO];
243+
self.shouldRejectSystemScroll = YES;
244+
// 用 dispatch 延迟一下,因为在文字发生换行时,系统自己会做一些滚动,我们要延迟一点才能避免被系统的滚动覆盖
245+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
246+
self.shouldRejectSystemScroll = NO;
247+
[textView setContentOffset:CGPointMake(textView.contentOffset.x, contentOffsetY) animated:NO];
248+
});
247249
}
248-
self.textChangedBecauseOfPaste = NO;
249250
}
250251
}
251252

252253
- (CGSize)sizeThatFits:(CGSize)size {
253-
// iOS 8 调用 sizeThatFits: 会导致文字跳动,因此自己计算 https://github.com/QMUI/QMUI_iOS/issues/92
254-
if (IOS_VERSION < 9.0 && IOS_VERSION >= 8.0 && self.callingSizeThatFitsByAutoResizable) {
254+
// iOS 10 以下调用 sizeThatFits: 会导致文字跳动,因此自己计算 https://github.com/QMUI/QMUI_iOS/issues/92
255+
if (IOS_VERSION < 10.0 && self.callingSizeThatFitsByAutoResizable) {
255256
CGFloat contentWidth = size.width - UIEdgeInsetsGetHorizontalValue(self.textContainerInset) - UIEdgeInsetsGetHorizontalValue(self.contentInset);
256257
CGRect textRect = [self.attributedText boundingRectWithSize:CGSizeMake(contentWidth, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil];
257258
CGSize resultSize = CGSizeMake(size.width, CGRectGetHeight(textRect) + UIEdgeInsetsGetVerticalValue(self.textContainerInset) + UIEdgeInsetsGetVerticalValue(self.contentInset));
@@ -287,11 +288,6 @@ - (void)updatePlaceholderLabelHidden {
287288
}
288289
}
289290

290-
- (void)paste:(id)sender {
291-
self.textChangedBecauseOfPaste = YES;
292-
[super paste:sender];
293-
}
294-
295291
- (NSUInteger)lengthWithString:(NSString *)string {
296292
return self.shouldCountingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length;
297293
}
@@ -422,6 +418,24 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
422418
}
423419
}
424420

421+
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated {
422+
if (!self.shouldRejectSystemScroll) {
423+
[super setContentOffset:contentOffset animated:animated];
424+
if (self.debug) NSLog(@"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
425+
} else {
426+
if (self.debug) NSLog(@"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
427+
}
428+
}
429+
430+
- (void)setContentOffset:(CGPoint)contentOffset {
431+
if (!self.shouldRejectSystemScroll) {
432+
[super setContentOffset:contentOffset];
433+
if (self.debug) NSLog(@"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
434+
} else {
435+
if (self.debug) NSLog(@"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
436+
}
437+
}
438+
425439
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
426440
if ([self.originalDelegate respondsToSelector:_cmd]) {
427441
[self.originalDelegate scrollViewDidZoom:scrollView];

0 commit comments

Comments
 (0)