杨锴
2025-04-16 09a372bc45fde16fd42257ab6f78b8deeecf720b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/**
 * 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.
 */
 
//
//  QMUITableViewCell.m
//  qmui
//
//  Created by QMUI Team on 14-7-7.
//
 
#import "QMUITableViewCell.h"
#import "QMUICore.h"
#import "QMUIButton.h"
#import "UITableView+QMUI.h"
#import "UITableViewCell+QMUI.h"
 
@interface QMUITableViewCell() <UIScrollViewDelegate>
 
@property(nonatomic, assign) BOOL initByTableView;
@property(nonatomic, assign, readwrite) QMUITableViewCellPosition cellPosition;
@property(nonatomic, assign, readwrite) UITableViewCellStyle style;
@property(nonatomic, strong) UIImageView *defaultAccessoryImageView;
@property(nonatomic, strong) QMUIButton *defaultAccessoryButton;
@property(nonatomic, strong) UIView *defaultDetailDisclosureView;
@end
 
@implementation QMUITableViewCell
 
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        if (!self.initByTableView) {
            [self didInitializeWithStyle:style];
        }
    }
    return self;
}
 
- (instancetype)initForTableView:(UITableView *)tableView withStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self.initByTableView = YES;
    if (self = [self initWithStyle:style reuseIdentifier:reuseIdentifier]) {// 这里需要调用 self 的 initWithStyle,而不是 super,目的是为了让业务在重写 init 方法时可以沿用系统默认的思路,去重写 initWithStyle:reuseIdentifier:,但在 vc 里使用 cell 时又可以直接调用 initForTableView:withStyle:。
        self.parentTableView = tableView;
        [self didInitializeWithStyle:style];// 因为设置了 parentTableView,样式可能都需要变,所以这里重新执行一次 didInitializeWithStyle: 里的 qmui_styledAsQMUITableViewCell
    }
    return self;
}
 
- (instancetype)initForTableView:(UITableView *)tableView withReuseIdentifier:(NSString *)reuseIdentifier {
    return [self initForTableView:tableView withStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
}
 
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        [self didInitializeWithStyle:UITableViewCellStyleDefault];
    }
    return self;
}
 
// layoutSubviews 里不可以拿 textLabel 的 minX 来设置 separatorInset,如果要设置只能写死一个值,否则会导致 textLabel 的 minX 逐渐叠加从而使 textLabel 被移出屏幕外
- (void)layoutSubviews {
    [super layoutSubviews];
    
    BOOL hasCustomAccessoryEdgeInset = self.accessoryView.superview && !UIEdgeInsetsEqualToEdgeInsets(self.accessoryEdgeInsets, UIEdgeInsetsZero);
    if (hasCustomAccessoryEdgeInset) {
        CGRect accessoryViewOldFrame = self.accessoryView.frame;
        accessoryViewOldFrame = CGRectSetX(accessoryViewOldFrame, CGRectGetMinX(accessoryViewOldFrame) - self.accessoryEdgeInsets.right);
        accessoryViewOldFrame = CGRectSetY(accessoryViewOldFrame, CGRectGetMinY(accessoryViewOldFrame) + self.accessoryEdgeInsets.top - self.accessoryEdgeInsets.bottom);
        self.accessoryView.frame = accessoryViewOldFrame;
        
        CGRect contentViewOldFrame = self.contentView.frame;
        contentViewOldFrame = CGRectSetWidth(contentViewOldFrame, CGRectGetMinX(accessoryViewOldFrame) - self.accessoryEdgeInsets.left);
        self.contentView.frame = contentViewOldFrame;
    }
 
    if (self.style == UITableViewCellStyleDefault || self.style == UITableViewCellStyleSubtitle) {
        
        BOOL hasCustomImageEdgeInsets = self.imageView.image && !UIEdgeInsetsEqualToEdgeInsets(self.imageEdgeInsets, UIEdgeInsetsZero);
        
        BOOL hasCustomTextLabelEdgeInsets = self.textLabel.text.length > 0 && !UIEdgeInsetsEqualToEdgeInsets(self.textLabelEdgeInsets, UIEdgeInsetsZero);
        
        BOOL shouldChangeDetailTextLabelFrame = self.style == UITableViewCellStyleSubtitle;
        BOOL hasCustomDetailLabelEdgeInsets = shouldChangeDetailTextLabelFrame && self.detailTextLabel.text.length > 0 && !UIEdgeInsetsEqualToEdgeInsets(self.detailTextLabelEdgeInsets, UIEdgeInsetsZero);
        
        CGRect imageViewFrame = self.imageView.frame;
        CGRect textLabelFrame = self.textLabel.frame;
        CGRect detailTextLabelFrame = self.detailTextLabel.frame;
        
        if (hasCustomImageEdgeInsets) {
            imageViewFrame.origin.x += self.imageEdgeInsets.left - self.imageEdgeInsets.right;
            imageViewFrame.origin.y += self.imageEdgeInsets.top - self.imageEdgeInsets.bottom;
            
            textLabelFrame.origin.x += self.imageEdgeInsets.left;
            textLabelFrame.size.width = fmin(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame));
            
            if (shouldChangeDetailTextLabelFrame) {
                detailTextLabelFrame.origin.x += self.imageEdgeInsets.left;
                detailTextLabelFrame.size.width = fmin(CGRectGetWidth(detailTextLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(detailTextLabelFrame));
            }
        }
        if (hasCustomTextLabelEdgeInsets) {
            textLabelFrame.origin.x += self.textLabelEdgeInsets.left - self.textLabelEdgeInsets.right;
            textLabelFrame.origin.y += self.textLabelEdgeInsets.top - self.textLabelEdgeInsets.bottom;
            textLabelFrame.size.width = fmin(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame));
        }
        if (hasCustomDetailLabelEdgeInsets) {
            detailTextLabelFrame.origin.x += self.detailTextLabelEdgeInsets.left - self.detailTextLabelEdgeInsets.right;
            detailTextLabelFrame.origin.y += self.detailTextLabelEdgeInsets.top - self.detailTextLabelEdgeInsets.bottom;
            detailTextLabelFrame.size.width = fmin(CGRectGetWidth(detailTextLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(detailTextLabelFrame));
        }
        
        self.imageView.frame = imageViewFrame;
        self.textLabel.frame = textLabelFrame;
        self.detailTextLabel.frame = detailTextLabelFrame;
    }
    
    // 由于调整 accessoryEdgeInsets 可能会影响 contentView 的宽度,所以几个 subviews 的布局也要保护一下
    if (hasCustomAccessoryEdgeInset) {
        if (CGRectGetMaxX(self.textLabel.frame) > CGRectGetWidth(self.contentView.bounds)) {
            self.textLabel.frame = CGRectSetWidth(self.textLabel.frame, CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(self.textLabel.frame));
        }
        if (CGRectGetMaxX(self.detailTextLabel.frame) > CGRectGetWidth(self.contentView.bounds)) {
            self.detailTextLabel.frame = CGRectSetWidth(self.detailTextLabel.frame, CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(self.detailTextLabel.frame));
        }
    }
}
 
// QMUITableViewCell 由于 init 时就把 tableView 传进来,所以可以在更早的时机拿到 qmui_tableView 的值,如果是系统的 UITableView,默认只能在添加到 tableView 上之后才可以获取到引用
- (UITableView *)qmui_tableView {
    return self.parentTableView ?: [super qmui_tableView];
}
 
- (void)setEnabled:(BOOL)enabled {
    if (_enabled != enabled) {
        if (enabled) {
            self.userInteractionEnabled = YES;
            UIColor *titleLabelColor = self.qmui_styledTextLabelColor;
            if (titleLabelColor) {
                self.textLabel.textColor = titleLabelColor;
            }
            UIColor *detailLabelColor = self.qmui_styledDetailTextLabelColor;
            if (detailLabelColor) {
                self.detailTextLabel.textColor = detailLabelColor;
            }
        } else {
            self.userInteractionEnabled = NO;
            UIColor *disabledColor = UIColorDisabled;
            if (disabledColor) {
                self.textLabel.textColor = disabledColor;
                self.detailTextLabel.textColor = disabledColor;
            }
        }
        _enabled = enabled;
    }
}
 
- (void)initDefaultAccessoryImageViewIfNeeded {
    if (!self.defaultAccessoryImageView) {
        self.defaultAccessoryImageView = [[UIImageView alloc] init];
        self.defaultAccessoryImageView.contentMode = UIViewContentModeCenter;
    }
}
 
- (void)initDefaultAccessoryButtonIfNeeded {
    if (!self.defaultAccessoryButton) {
        self.defaultAccessoryButton = [[QMUIButton alloc] init];
        [self.defaultAccessoryButton addTarget:self action:@selector(handleAccessoryButtonEvent:) forControlEvents:UIControlEventTouchUpInside];
        self.defaultAccessoryButton.accessibilityLabel = @"更多信息";
    }
}
 
- (void)initDefaultDetailDisclosureViewIfNeeded {
    if (!self.defaultDetailDisclosureView) {
        self.defaultDetailDisclosureView = [[UIView alloc] init];
    }
}
 
// 重写accessoryType,如果是UITableViewCellAccessoryDisclosureIndicator类型的,则使用 QMUIConfigurationTemplate.m 配置表里的图片
- (void)setAccessoryType:(UITableViewCellAccessoryType)accessoryType {
    [super setAccessoryType:accessoryType];
    
    if (accessoryType == UITableViewCellAccessoryDisclosureIndicator) {
        UIImage *indicatorImage = TableViewCellDisclosureIndicatorImage;
        if (indicatorImage) {
            [self initDefaultAccessoryImageViewIfNeeded];
            self.defaultAccessoryImageView.image = indicatorImage;
            [self.defaultAccessoryImageView sizeToFit];
            self.accessoryView = self.defaultAccessoryImageView;
            return;
        }
    }
    
    if (accessoryType == UITableViewCellAccessoryCheckmark) {
        UIImage *checkmarkImage = TableViewCellCheckmarkImage;
        if (checkmarkImage) {
            [self initDefaultAccessoryImageViewIfNeeded];
            self.defaultAccessoryImageView.image = checkmarkImage;
            [self.defaultAccessoryImageView sizeToFit];
            self.accessoryView = self.defaultAccessoryImageView;
            return;
        }
    }
    
    if (accessoryType == UITableViewCellAccessoryDetailButton) {
        UIImage *detailButtonImage = TableViewCellDetailButtonImage;
        if (detailButtonImage) {
            [self initDefaultAccessoryButtonIfNeeded];
            [self.defaultAccessoryButton setImage:detailButtonImage forState:UIControlStateNormal];
            [self.defaultAccessoryButton sizeToFit];
            self.accessoryView = self.defaultAccessoryButton;
            return;
        }
    }
    
    if (accessoryType == UITableViewCellAccessoryDetailDisclosureButton) {
        UIImage *detailButtonImage = TableViewCellDetailButtonImage;
        UIImage *indicatorImage = TableViewCellDisclosureIndicatorImage;
        
        if (detailButtonImage) {
            QMUIAssert(!!indicatorImage, NSStringFromClass(self.class), @"TableViewCellDetailButtonImage 和 TableViewCellDisclosureIndicatorImage 必须同时使用,但目前后者为 nil");
            [self initDefaultDetailDisclosureViewIfNeeded];
            [self initDefaultAccessoryButtonIfNeeded];
            [self.defaultAccessoryButton setImage:detailButtonImage forState:UIControlStateNormal];
            [self.defaultAccessoryButton sizeToFit];
            if (self.accessoryView == self.defaultAccessoryButton) {
                self.accessoryView = nil;
            }
            [self.defaultDetailDisclosureView addSubview:self.defaultAccessoryButton];
        }
        
        if (indicatorImage) {
            QMUIAssert(!!detailButtonImage, NSStringFromClass(self.class), @"TableViewCellDetailButtonImage 和 TableViewCellDisclosureIndicatorImage 必须同时使用,但目前前者为 nil");
            [self initDefaultDetailDisclosureViewIfNeeded];
            [self initDefaultAccessoryImageViewIfNeeded];
            self.defaultAccessoryImageView.image = indicatorImage;
            [self.defaultAccessoryImageView sizeToFit];
            if (self.accessoryView == self.defaultAccessoryImageView) {
                self.accessoryView = nil;
            }
            [self.defaultDetailDisclosureView addSubview:self.defaultAccessoryImageView];
        }
        
        if (indicatorImage && detailButtonImage) {
            CGFloat spacingBetweenDetailButtonAndIndicatorImage = TableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator;
            self.defaultDetailDisclosureView.frame = CGRectFlatMake(CGRectGetMinX(self.defaultDetailDisclosureView.frame), CGRectGetMinY(self.defaultDetailDisclosureView.frame), CGRectGetWidth(self.defaultAccessoryButton.frame) + spacingBetweenDetailButtonAndIndicatorImage + CGRectGetWidth(self.defaultAccessoryImageView.frame), fmax(CGRectGetHeight(self.defaultAccessoryButton.frame), CGRectGetHeight(self.defaultAccessoryImageView.frame)));
            self.defaultAccessoryButton.frame = CGRectSetXY(self.defaultAccessoryButton.frame, 0, CGRectGetMinYVerticallyCenterInParentRect(self.defaultDetailDisclosureView.frame, self.defaultAccessoryButton.frame));
            self.defaultAccessoryImageView.frame = CGRectSetXY(self.defaultAccessoryImageView.frame, CGRectGetMaxX(self.defaultAccessoryButton.frame) + spacingBetweenDetailButtonAndIndicatorImage, CGRectGetMinYVerticallyCenterInParentRect(self.defaultDetailDisclosureView.frame, self.defaultAccessoryImageView.frame));
            self.accessoryView = self.defaultDetailDisclosureView;
            return;
        }
    }
    
    self.accessoryView = nil;
}
 
#pragma mark - <UIScrollViewDelegate>
 
// 为了修复因优化accessoryView导致的向左滑动cell容易触发accessoryView事件 a little dirty by molice
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    self.accessoryView.userInteractionEnabled = NO;
}
 
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    self.accessoryView.userInteractionEnabled = YES;
}
 
#pragma mark - Touch Event
 
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    if (!view) {
        return nil;
    }
    // 对于使用自定义的accessoryView的情况,扩大其响应范围。最小范围至少是一个靠在屏幕右边缘的“宽高都为cell高度”的正方形区域
    if (self.accessoryView
        && !self.accessoryView.hidden
        && self.accessoryView.userInteractionEnabled
        && !self.editing
        // UISwitch被点击时,[super hitTest:point withEvent:event]返回的不是UISwitch,而是它的一个subview,如果这里直接返回UISwitch会导致控件无法使用,因此对UISwitch做特殊屏蔽
        && ![self.accessoryView isKindOfClass:[UISwitch class]]
        ) {
        
        CGRect accessoryViewFrame = self.accessoryView.frame;
        CGRect responseEventFrame;
        responseEventFrame.origin.x = CGRectGetMinX(accessoryViewFrame) + self.accessoryHitTestEdgeInsets.left;
        responseEventFrame.origin.y = CGRectGetMinY(accessoryViewFrame) + self.accessoryHitTestEdgeInsets.top;
        responseEventFrame.size.width = CGRectGetWidth(accessoryViewFrame) + UIEdgeInsetsGetHorizontalValue(self.accessoryHitTestEdgeInsets);
        responseEventFrame.size.height = CGRectGetHeight(accessoryViewFrame) + UIEdgeInsetsGetVerticalValue(self.accessoryHitTestEdgeInsets);
        if (CGRectContainsPoint(responseEventFrame, point)) {
            return self.accessoryView;
        }
    }
    return view;
}
 
- (void)handleAccessoryButtonEvent:(QMUIButton *)detailButton {
    if ([self.qmui_tableView.delegate respondsToSelector:@selector(tableView:accessoryButtonTappedForRowWithIndexPath:)]) {
        [self.qmui_tableView.delegate tableView:self.qmui_tableView accessoryButtonTappedForRowWithIndexPath:[self.qmui_tableView qmui_indexPathForRowAtView:detailButton]];
    }
}
 
@end
 
@implementation QMUITableViewCell(QMUISubclassingHooks)
 
- (void)didInitializeWithStyle:(UITableViewCellStyle)style {
    self.initByTableView = NO;
    _cellPosition = QMUITableViewCellPositionNone;
    
    _style = style;
    _enabled = YES;
    _accessoryHitTestEdgeInsets = UIEdgeInsetsMake(-12, -12, -12, -12);
    
    // TODO: molice 测一下时至今日还需要吗?
    // 因为在hitTest里扩大了accessoryView的响应范围,因此提高了系统一个与此相关的bug的出现几率,所以又在scrollView.delegate里做一些补丁性质的东西来修复
    if ([self.subviews.firstObject isKindOfClass:[UIScrollView class]]) {
        UIScrollView *scrollView = (UIScrollView *)[self.subviews objectAtIndex:0];
        scrollView.delegate = self;
    }
    
    [self qmui_styledAsQMUITableViewCell];
}
 
- (void)updateCellAppearanceWithIndexPath:(NSIndexPath *)indexPath {
    // 子类继承
    if (indexPath && self.qmui_tableView) {
        QMUITableViewCellPosition position = [self.qmui_tableView qmui_positionForRowAtIndexPath:indexPath];
        self.cellPosition = position;
    } else {
        self.cellPosition = QMUITableViewCellPositionNone;
    }
}
 
@end