杨锴
2025-06-04 ac84f81ca2311300b431c1bfb9f71253b59073f2
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
/**
 * 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.
 */
 
//
//  QMUIImagePreviewViewController.m
//  qmui
//
//  Created by QMUI Team on 2016/11/30.
//
 
#import "QMUIImagePreviewViewController.h"
#import "QMUICore.h"
#import "QMUIImagePreviewViewTransitionAnimator.h"
#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 initAppearance];
    });
}
 
+ (void)initAppearance {
    QMUIImagePreviewViewController.appearance.backgroundColor = UIColorBlack;
}
 
@end
 
@interface QMUIImagePreviewViewController ()
 
@property(nonatomic, strong) UIPanGestureRecognizer *dismissingGesture;
@property(nonatomic, assign) CGPoint gestureBeganLocation;
@property(nonatomic, weak) QMUIZoomImageView *gestureZoomImageView;
@property(nonatomic, assign) BOOL canShowPresentingViewControllerWhenGesturing;
@property(nonatomic, assign) BOOL originalStatusBarHidden;
@property(nonatomic, assign) BOOL statusBarHidden;
@end
 
@implementation QMUIImagePreviewViewController
 
- (void)didInitialize {
    [super didInitialize];
    
    self.sourceImageCornerRadius = QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension;
    
    _dismissingGestureEnabled = YES;
    
    [self qmui_applyAppearance];
    
    self.qmui_prefersHomeIndicatorAutoHiddenBlock = ^BOOL{
        return YES;
    };
 
    
    // present style
    self.transitioningAnimator = [[QMUIImagePreviewViewTransitionAnimator alloc] init];
    self.modalPresentationStyle = UIModalPresentationCustom;
    self.modalPresentationCapturesStatusBarAppearance = YES;
    self.transitioningDelegate = self;
}
 
- (void)setBackgroundColor:(UIColor *)backgroundColor {
    _backgroundColor = backgroundColor;
    if ([self isViewLoaded]) {
        self.view.backgroundColor = backgroundColor;
    }
}
 
@synthesize imagePreviewView = _imagePreviewView;
- (QMUIImagePreviewView *)imagePreviewView {
    if (!_imagePreviewView) {
        _imagePreviewView = [[QMUIImagePreviewView alloc] initWithFrame:self.isViewLoaded ? self.view.bounds : CGRectZero];
    }
    return _imagePreviewView;
}
 
- (void)initSubviews {
    [super initSubviews];
    self.view.backgroundColor = self.backgroundColor;
    [self.view addSubview:self.imagePreviewView];
}
 
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    self.imagePreviewView.qmui_frameApplyTransform = self.view.bounds;
    
    UIViewController *backendViewController = [self visibleViewControllerWithViewController:self.presentingViewController];
    self.canShowPresentingViewControllerWhenGesturing = [QMUIHelper interfaceOrientationMask:backendViewController.supportedInterfaceOrientations containsInterfaceOrientation:UIApplication.sharedApplication.statusBarOrientation];
}
 
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    if (self.qmui_isPresented) {
        [self initObjectsForZoomStyleIfNeeded];
    }
    [self.imagePreviewView.collectionView reloadData];
    [self.imagePreviewView.collectionView layoutIfNeeded];
}
 
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    if (self.qmui_isPresented) {
        self.statusBarHidden = YES;
    }
    [self setNeedsStatusBarAppearanceUpdate];
}
 
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.statusBarHidden = self.originalStatusBarHidden;
    [self setNeedsStatusBarAppearanceUpdate];
}
 
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self removeObjectsForZoomStyle];
    [self resetDismissingGesture];
}
 
- (void)setPresentingStyle:(QMUIImagePreviewViewControllerTransitioningStyle)presentingStyle {
    _presentingStyle = presentingStyle;
    self.dismissingStyle = presentingStyle;
}
 
- (void)setTransitioningAnimator:(__kindof QMUIImagePreviewViewTransitionAnimator *)transitioningAnimator {
    _transitioningAnimator = transitioningAnimator;
    transitioningAnimator.imagePreviewViewController = self;
}
 
- (BOOL)prefersStatusBarHidden {
    if (self.qmui_visibleState < QMUIViewControllerDidAppear || self.qmui_visibleState >= QMUIViewControllerDidDisappear) {
        // 在 present/dismiss 动画过程中,都使用原界面的状态栏显隐状态
        if (self.presentingViewController) {
            BOOL statusBarHidden = self.presentingViewController.view.window.windowScene.statusBarManager.statusBarHidden;
            self.originalStatusBarHidden = statusBarHidden;
            return self.originalStatusBarHidden;
        }
        return [super prefersStatusBarHidden];
    }
    return self.statusBarHidden;
}
 
#pragma mark - 动画
 
- (void)initObjectsForZoomStyleIfNeeded {
    if (!self.dismissingGesture && self.dismissingGestureEnabled) {
        self.dismissingGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDismissingPreviewGesture:)];
        [self.view addGestureRecognizer:self.dismissingGesture];
    }
}
 
- (void)removeObjectsForZoomStyle {
    [self.dismissingGesture removeTarget:self action:@selector(handleDismissingPreviewGesture:)];
    [self.view removeGestureRecognizer:self.dismissingGesture];
    self.dismissingGesture = nil;
}
 
- (void)handleDismissingPreviewGesture:(UIPanGestureRecognizer *)gesture {
    
    if (!self.dismissingGestureEnabled) return;
    
    switch (gesture.state) {
        case UIGestureRecognizerStateBegan:
            self.gestureBeganLocation = [gesture locationInView:self.view];
            self.gestureZoomImageView = [self.imagePreviewView zoomImageViewAtIndex:self.imagePreviewView.currentImageIndex];
            self.gestureZoomImageView.scrollView.clipsToBounds = NO;// 当 contentView 被放大后,如果不去掉 clipToBounds,那么手势退出预览时,contentView 溢出的那部分内容就看不到
            break;
            
        case UIGestureRecognizerStateChanged: {
            CGPoint location = [gesture locationInView:self.view];
            CGFloat horizontalDistance = location.x - self.gestureBeganLocation.x;
            CGFloat verticalDistance = location.y - self.gestureBeganLocation.y;
            CGFloat ratio = 1.0;
            CGFloat alpha = 1.0;
            if (verticalDistance > 0) {
                // 往下拉的话,图片缩小,但图片移动距离与手指移动距离保持一致
                ratio = 1.0 - verticalDistance / CGRectGetHeight(self.view.bounds) / 2;
                
                // 如果预览大图支持横竖屏而背后的界面只支持竖屏,则在横屏时手势拖拽不要露出背后的界面
                if (self.canShowPresentingViewControllerWhenGesturing) {
                    alpha = 1.0 - verticalDistance / CGRectGetHeight(self.view.bounds) * 1.8;
                }
            } else {
                // 往上拉的话,图片不缩小,但手指越往上移动,图片将会越难被拖走
                CGFloat a = self.gestureBeganLocation.y + 100;// 后面这个加数越大,拖动时会越快达到不怎么拖得动的状态
                CGFloat b = 1 - pow((a - fabs(verticalDistance)) / a, 2);
                CGFloat contentViewHeight = CGRectGetHeight(self.gestureZoomImageView.contentViewRectInZoomImageView);
                CGFloat c = (CGRectGetHeight(self.view.bounds) - contentViewHeight) / 2;
                verticalDistance = -c * b;
            }
            CGAffineTransform transform = CGAffineTransformMakeTranslation(horizontalDistance, verticalDistance);
            transform = CGAffineTransformScale(transform, ratio, ratio);
            self.gestureZoomImageView.transform = transform;
            self.view.backgroundColor = [self.view.backgroundColor colorWithAlphaComponent:alpha];
            BOOL statusBarHidden = alpha >= 1 ? YES : self.originalStatusBarHidden;
            if (statusBarHidden != self.statusBarHidden) {
                self.statusBarHidden = statusBarHidden;
                [self setNeedsStatusBarAppearanceUpdate];
            }
        }
            break;
            
        case UIGestureRecognizerStateEnded: {
            CGPoint location = [gesture locationInView:self.view];
            CGFloat verticalDistance = location.y - self.gestureBeganLocation.y;
            if (verticalDistance > CGRectGetHeight(self.view.bounds) / 2 / 3) {
                
                // 如果背后的界面支持的方向与当前预览大图的界面不一样,则为了避免在 dismiss 后看到背后界面的旋转,这里提前触发背后界面的 viewWillAppear,从而借助 AutomaticallyRotateDeviceOrientation 的功能去提前旋转到正确方向。(备忘,如果不这么处理,标准的触发 viewWillAppear: 的时机是在 animator 的 animateTransition: 时,这里就算重复调用一次也不会导致 viewWillAppear: 多次触发)
                // 这里只能解决手势拖拽的 dismiss,如果是业务代码手动调用 dismiss 则无法兼顾,再看怎么处理。
                if (!self.canShowPresentingViewControllerWhenGesturing) {
                    [self.presentingViewController beginAppearanceTransition:YES animated:YES];
                }
                
                [self dismissViewControllerAnimated:YES completion:nil];
            } else {
                [self cancelDismissingGesture];
            }
        }
            break;
        default:
            [self cancelDismissingGesture];
            break;
    }
}
 
// 手势判定失败,恢复到手势前的状态
- (void)cancelDismissingGesture {
    self.statusBarHidden = YES;
    [UIView animateWithDuration:.2 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        [self setNeedsStatusBarAppearanceUpdate];
        [self resetDismissingGesture];
    } completion:NULL];
}
 
// 清理手势相关的变量
- (void)resetDismissingGesture {
    self.gestureZoomImageView.transform = CGAffineTransformIdentity;
    self.gestureBeganLocation = CGPointZero;
    self.gestureZoomImageView = nil;
    self.view.backgroundColor = self.backgroundColor;
}
 
// 不使用 qmui_visibleViewControllerIfExist 是因为不想考虑 presentedViewController
- (UIViewController *)visibleViewControllerWithViewController:(UIViewController *)viewController {
    if ([viewController isKindOfClass:[UINavigationController class]]) {
        return [self visibleViewControllerWithViewController:((UINavigationController *)viewController).topViewController];
    }
    
    if ([viewController isKindOfClass:[UITabBarController class]]) {
        return [self visibleViewControllerWithViewController:((UITabBarController *)viewController).selectedViewController];
    }
    
    return viewController;
}
 
#pragma mark - <UIViewControllerTransitioningDelegate>
 
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return self.transitioningAnimator;
}
 
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return self.transitioningAnimator;
}
 
@end