1.开篇哔哔之言:
最近在项目中有一个发朋友圈的功能,然后就自主仿了一波,微信的功能,然后就发现了,全图浏览时的一个小动画效果,顿时想玩玩,也算是小经波折,言归正传,走起。。。
2.动画效果的简介:
当单击屏幕时,导航栏和状态栏整体的上移一段距离后,渐变消失,两种实现方式:完全的系统自带方法实现(不完美),另外一种自定义导航栏+获取状态栏(自认完美-接近微信效果)。-如果有错,请帮忙纠正!!!!
—、 使用系统方法实现(不完美):
先上全部代码,再解释!!!
.h文件
#import <UIKit/UIKit.h>
typedef void(^SingleTapBlock)();
@interface YSShowView : UIView
/** 显示的图片 */
@property (nonatomic, strong) UIImage * img;
/** 点击事件 */
@property (nonatomic, copy) SingleTapBlock tapBlock;
@end
typedef void(^DeleteBlock)(UIImage * img);
@interface SLBrowseBigImgController : UIViewController
/** 单列 */
+ (instancetype)sharedInstance;
/** 图片的数据源 */
@property (nonatomic, strong) NSArray * imgArrs;
/** 开始的下标 */
@property (nonatomic, assign) NSInteger from;
/** 删除图片的block */
@property (nonatomic, copy) DeleteBlock deleteBlock;
@end
.m文件
#import "SLBrowseBigImgController.h"
@interface YSShowView () <UIScrollViewDelegate>
{
CATransition *_animation; //缩放动画效果
CGFloat _scaleNum; //图片放大倍数
}
/** 显示的图片 */
@property (nonatomic, strong) UIImageView * imgView;
/** 用于捏合放大与缩小的scrollView */
@property(nonatomic,strong)UIScrollView *scrollview;
@end
@implementation YSShowView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_scaleNum = 1;
[self createUI];
[self addNotification];
}
return self;
}
- (void)createUI {
self.backgroundColor = [UIColor blackColor];
[self addSubview:self.scrollview];
[self.scrollview addSubview:self.imgView];
//设置UIScrollView的滚动范围和图片的真实尺寸一致
self.scrollview.contentSize = self.imgView.frame.size;
}
- (void)addNotification {
// 双击手势
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleDoubleTap:)];
doubleTap.numberOfTapsRequired = 2;
doubleTap.numberOfTouchesRequired = 1;
[self addGestureRecognizer:doubleTap];
}
- (void)layoutSubviews {
[super layoutSubviews];
NSLog(@"KScreenWidth:%g -- KScreenHeight:%g",KScreenWidth,KScreenHeight);
CGFloat imgWidth = _img.size.width;
CGFloat imgHeight = _img.size.height;
self.imgView.frame = CGRectMake(0, 0, KScreenWidth, KScreenWidth * imgHeight/imgWidth);
self.imgView.center = CGPointMake(KScreenWidth*0.5, KScreenHeight*0.5);
// 设置scrollView的frame
self.scrollview.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
}
#pragma mark - 处理双击手势
- (void)handleDoubleTap:(UIGestureRecognizer *)sender {
if (_scaleNum >= 1 && _scaleNum <= 2) {
_scaleNum++;
}else {
_scaleNum = 1;
}
[self.scrollview setZoomScale:_scaleNum animated:YES];
}
#pragma mark - UIScrollViewDelegate,告诉scrollview要缩放的是哪个子控件
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imgView;
}
#pragma mark - 等比例放大,让放大的图片保持在scrollView的中央
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
NSLog(@"bounds:%@ -- contentSize:%@",NSStringFromCGRect(scrollView.bounds),NSStringFromCGSize(scrollView.contentSize));
CGFloat offsetX = (self.scrollview.bounds.size.width > self.scrollview.contentSize.width)?(self.scrollview.bounds.size.width - self.scrollview.contentSize.width) *0.5 : 0.0;
CGFloat offsetY = (self.scrollview.bounds.size.height > self.scrollview.contentSize.height)?
(self.scrollview.bounds.size.height - self.scrollview.contentSize.height) *0.5 : 0.0;
self.imgView.center = CGPointMake(self.scrollview.contentSize.width *0.5 + offsetX,self.scrollview.contentSize.height *0.5 + offsetY);
}
- (void)setImg:(UIImage *)img {
_img = img;
self.imgView.image = img;
// 更新布局
[self layoutSubviews];
}
#pragma mark - Lazy load
- (UIScrollView *)scrollview {
if (!_scrollview) {
//添加捏合手势,放大与缩小图片
_scrollview = [[UIScrollView alloc] initWithFrame:CGRectMake(0,0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
//设置实现缩放
//设置代理scrollview的代理对象
_scrollview.delegate = self;
//设置最大伸缩比例
_scrollview.maximumZoomScale = 3;
//设置最小伸缩比例
_scrollview.minimumZoomScale = 1;
[_scrollview setZoomScale:1 animated:NO];
_scrollview.scrollsToTop = NO;
_scrollview.scrollEnabled = YES;
_scrollview.showsHorizontalScrollIndicator = NO;
_scrollview.showsVerticalScrollIndicator = NO;
}
return _scrollview;
}
- (UIImageView *)imgView {
if (!_imgView) {
_imgView = [[UIImageView alloc] init];
_imgView.userInteractionEnabled = true;
}
return _imgView;
}
@end
@interface SLBrowseBigImgController () <UIScrollViewDelegate>
{
BOOL _statusBarHidden;
}
/** 图片切换的过渡效果 */
@property (nonatomic, strong) UIScrollView * scrollView;
/** 保存所有的showView */
@property (nonatomic, strong) NSMutableArray <YSShowView *>* showViewArray;
/** 显示当前是第几张的label */
@property (nonatomic, strong) UILabel * titleLabel;
@end
@implementation SLBrowseBigImgController
// 创建静态对象 防止外部访问
static SLBrowseBigImgController *_instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
// @synchronized (self) {
// // 为了防止多线程同时访问对象,造成多次分配内存空间,所以要加上线程锁
// if (_instance == nil) {
// _instance = [super allocWithZone:zone];
// }
// return _instance;
// }
// 也可以使用一次性代码
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (_instance == nil) {
_instance = [super allocWithZone:zone];
}
});
return _instance;
}
// 为了使实例易于外界访问 我们一般提供一个类方法
// 类方法命名规范 share类名|default类名|类名
+ (instancetype)sharedInstance {
//return _instance;
// 最好用self 用Tools他的子类调用时会出现错误
return [[self alloc]init];
}
// 为了严谨,也要重写copyWithZone 和 mutableCopyWithZone
- (id)copyWithZone:(NSZone *)zone {
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return _instance;
}
#pragma mark - 隐藏状态栏
- (BOOL)prefersStatusBarHidden {
return _statusBarHidden;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation {
return UIStatusBarAnimationFade;
}
- (void)viewDidLoad {
[super viewDidLoad];
// 添加导航栏右侧的btn
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]initWithCustomView:[self createBtnWithAction:@selector(deleteImgEvent)]];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// 关闭系统自适应的高度
self.automaticallyAdjustsScrollViewInsets = false;
// 禁用返回手势
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = false;
}
// 添加只执行一次添加tap事件
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self.navigationController.barHideOnTapGestureRecognizer addTarget:self action:@selector(tapAction:)];
});
// 开启当点击屏幕时,隐藏导航栏的tap事件
self.navigationController.hidesBarsOnTap = true;
// 隐藏导航栏
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setStatusBarAndNavBar:true];
});
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// 开启返回手势
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = true;
}
// 显示导航栏的tap事件
self.navigationController.hidesBarsOnTap = false;
}
// 让状态栏跟随导航栏一起消失
- (void)tapAction:(UITapGestureRecognizer *)recognizer {
_statusBarHidden = !_statusBarHidden;
// 更新状态栏
[self setNeedsStatusBarAppearanceUpdate];
}
- (void)setStatusBarAndNavBar:(BOOL)isHidden {
if (!isHidden) {
// 导航栏
self.navigationController.navigationBarHidden = false;
// 状态栏
_statusBarHidden = false;
}else {
// 导航栏
self.navigationController.navigationBarHidden = true;
// 状态栏
_statusBarHidden = true;
}
[self setNeedsStatusBarAppearanceUpdate];
}
- (void)createShowView {
// 在scrollView上添加对应的showView
YSShowView *lastView = nil;
for (int i = 0; i < self.imgArrs.count; i++) {
YSShowView *showView = [[YSShowView alloc] init];
showView.img = [self.imgArrs objectAtIndex:i];
[self.scrollView addSubview:showView];
// 布局
[self.showViewArray addObject:showView];
[showView mas_makeConstraints:^(MASConstraintMaker *make) {
if (i == 0) {
make.top.left.equalTo(self.scrollView);
make.size.mas_equalTo(CGSizeMake(KScreenWidth, KScreenHeight));
}else {
make.top.equalTo(self.scrollView.mas_top);
make.left.equalTo(lastView.mas_right);
make.size.mas_equalTo(CGSizeMake(KScreenWidth, KScreenHeight));
}
}];
lastView = showView;
}
// 设置scrollView的contentSize
self.scrollView.contentSize = CGSizeMake(KScreenWidth * self.imgArrs.count, 0);
// 设置scrollView的内容偏移
[self.scrollView setContentOffset:CGPointMake(_from *KScreenWidth, 0)];
// 设置显示第几张的title
self.title = [NSString stringWithFormat:@"%ld / %ld",_from+1,self.imgArrs.count];
}
- (void)setImgArrs:(NSArray *)imgArrs {
_imgArrs = imgArrs;
// 添加底部scrollView
if (!self.scrollView.superview) {
// 添加左右滑动切换图骗的scrollView
[self.view addSubview:self.scrollView];
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
}];
}
// 移除已存在的
[self clearExistView];
// 如果数据源数组为空直接返回
if (self.imgArrs.count == 0) {
return;
}
// 布局 - 在scrollView上添加对应的showView
[self createShowView];
}
- (void)clearExistView {
if (self.showViewArray.count == 0) {
return;
}
for (YSShowView *showView in self.showViewArray) {
if (showView.superview) {
[showView removeFromSuperview];
}
}
[self.showViewArray removeAllObjects];
}
- (void)deleteImgEvent {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:@"要删除这张照片吗?" preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"删除" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
if (self.deleteBlock) {
// 获取将要删除的照片
UIImage *deleteImg = [self.imgArrs objectAtIndex:_from];
// 删除数据源
self.deleteBlock(deleteImg);
// 当前的下标也需要减1
if (--_from < 0) {
_from = 0;
}
// 删除本界面数据源
NSMutableArray *temp = [NSMutableArray arrayWithArray:self.imgArrs];
[temp removeObject:deleteImg];
if (temp.count == 0) {
[self.navigationController popViewControllerAnimated:true];
}else {
self.imgArrs = temp;
}
}
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
}];
[alert addAction:cancelAction];
[alert addAction:okAction];
[self presentViewController:alert animated:true completion:nil];
}
- (UIButton *)createBtnWithAction:(SEL)action {
UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setBackgroundColor:[UIColor clearColor]];
[btn setImage:[UIImage imageNamed:@"delete"] forState:UIControlStateNormal];
[btn addTarget:self action:action forControlEvents:UIControlEventTouchUpInside];
[btn sizeToFit];
return btn;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
_from = scrollView.contentOffset.x / KScreenWidth;
if (_from < self.imgArrs.count && self.imgArrs.count > 1) {
self.title = [NSString stringWithFormat:@"%ld / %ld",_from+1,self.imgArrs.count];
}
}
#pragma mark - Lazy load
- (NSMutableArray<YSShowView *>*)showViewArray {
if (!_showViewArray) {
_showViewArray = [NSMutableArray array];
}
return _showViewArray;
}
- (UIScrollView *)scrollView {
if (!_scrollView) {
_scrollView = [[UIScrollView alloc] init];
_scrollView.scrollEnabled = true;
_scrollView.pagingEnabled = true;
_scrollView.showsHorizontalScrollIndicator = false;
_scrollView.showsVerticalScrollIndicator = false;
_scrollView.delegate = self;
}
return _scrollView;
}
- (void)dealloc {
NSLog(@"%s👋👋👋",__func__);
}
重点在于:
1.UINavigationController导航控制器有一个属性hidesBarsOnTap,可以控制器导航栏的显示和隐藏,当设置为true时,点击屏幕,就会有一个带的隐藏导航栏的动画效果,再次点击时,就会显示;
2.但是状态栏就会很傲娇的一动不动,此时就需要引入UINavigationController的另外一个属性barHideOnTapGestureRecognizer,可以给该属性添加一个tap事件,当点击屏幕时,导航栏在隐藏/显示的同时,也会触发该tap件事,就可以操作状态栏了。
3.使用系统的方式,显示或隐藏状态栏就得重写用到两个系统方法和调用另外一个方法: [self setNeedsStatusBarAppearanceUpdate] (更新状态栏)
#pragma mark - 隐藏状态栏
- (BOOL)prefersStatusBarHidden {
return _statusBarHidden;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation {
return UIStatusBarAnimationFade;
}
当调用[self setNeedsStatusBarAppearanceUpdate] 时,系统就会重调用上面两个方法,从而达到控制状态栏的隐藏和显示。
总结:虽然最终都达到了,隐藏导航栏和状态栏的效果,屏幕使用率最大化,但是,缺点有三:
1.状态栏和导航栏的隐藏和显示动画是不同步的,状态栏是瞬间显示和隐藏的,而导航栏可以自定义动画;
2.需要在控制器消失之前重置状态栏和导航栏,避免影响到其他的正常界面,且容易出导航栏错乱的bug;
3.当用到,按住最左边滑动返回前一个界面的功能时,前一个界面的导航栏会提前显示,并不会显示当前即将消失的导航栏;
二、自定义导航栏+获取状态栏(自认完美-接近微信效果)
先上关键代码(.m文件):
#import "TestingNavBarController.h"
#define KScreenWidth [UIScreen mainScreen].bounds.size.width
@interface TestingNavBarController ()
{
BOOL _isSelect;
}
/** 自定义导航栏 */
@property (nonatomic, strong) UINavigationBar * customNavBar;
/** 获取状态栏的父view */
@property (nonatomic, weak) UIView * statusBarSuperView;
/** 状态栏 */
@property (nonatomic, weak) UIView * statusBar;
@end
@implementation TestingNavBarController
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:true];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// 将状态栏添加到自定义的导航栏上
UIApplication *app = [UIApplication sharedApplication];
// 获取状态栏
self.statusBar = [app valueForKey:@"statusBar"];
if (self.statusBar) {
self.statusBarSuperView = self.statusBar.superview;
[self.customNavBar addSubview:self.statusBar];
// 获取状态栏的子view
NSArray *subViews = [[self.statusBar valueForKey:@"foregroundView"] subviews];
for (UIView *view in subViews) {
NSLog(@"====%@",view);
}
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.navigationController setNavigationBarHidden:false];
[self.statusBarSuperView addSubview:self.statusBar];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.customNavBar];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClickEvent)];
[self.view addGestureRecognizer:tap];
}
- (UINavigationBar *)customNavBar {
if (!_customNavBar) {
_customNavBar = [[UINavigationBar alloc] initWithFrame:CGRectMake(0, 0, KScreenWidth, 64)];
_customNavBar.backgroundColor = [UIColor lightGrayColor];
// 自定义导航栏的title,用UILabel实现
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 50, 44)];
titleLabel.text = @"自定义";
titleLabel.textColor = [UIColor blackColor];
titleLabel.font = [UIFont systemFontOfSize:18];
// 创建导航栏组件
UINavigationItem *navItem = [[UINavigationItem alloc] init];
// 设置自定义的title
navItem.titleView = titleLabel;
// 创建左侧按钮
UIBarButtonItem *leftButton = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStylePlain target:self action:@selector(leftButtonClick)];
leftButton.tintColor = [UIColor purpleColor];
// 创建右侧按钮
UIBarButtonItem *rightButton = [[UIBarButtonItem alloc] initWithTitle:@"确定" style:UIBarButtonItemStylePlain target:self action:@selector(rightButtonClick)];
rightButton.tintColor = [UIColor orangeColor];
// 添加左侧、右侧按钮
[navItem setLeftBarButtonItem:leftButton animated:false];
[navItem setRightBarButtonItem:rightButton animated:false];
// 把导航栏组件加入导航栏
[_customNavBar pushNavigationItem:navItem animated:false];
}
return _customNavBar;
}
- (void)leftButtonClick {
[self.navigationController popViewControllerAnimated:true];
}
- (void)rightButtonClick {
}
- (void)tapClickEvent {
_isSelect = !_isSelect;
if (_isSelect) {
[UIView animateWithDuration:0.5 animations:^{
CGRect frame = self.customNavBar.frame;
frame.origin.y = -64;
self.customNavBar.frame = frame;
}];
}else {
[UIView animateWithDuration:0.5 animations:^{
CGRect frame = self.customNavBar.frame;
frame.origin.y = 0;
self.customNavBar.frame = frame;
}];
}
}
重点在于:
获取状态栏:self.statusBar = [[UIApplication sharedApplication] valueForKey:@"statusBar"],然后添加到自定义的导航栏上,就可以实现整体的隐藏/显示了,此时有两点切记:
1.获取并添加状态栏到自定义的导航栏的时机,必须是,在视图加载完后添加,否则在push到该界面时,会看到前一个界面的状态栏消失了,而且,状态栏的显示宽度会随控制器的宽度渐渐显示;
总结:该方法较第一种方式,需要手动控制的对象和对象属性比较少,而且,目前没有bug;用该方法替换掉第一种方式,就是一个浏览全图的功能了。。。