//
|
// MWPhotoBrowser.m
|
// MWPhotoBrowser
|
//
|
// Created by Michael Waterfall on 14/10/2010.
|
// Copyright 2010 d3i. All rights reserved.
|
//
|
|
#import <QuartzCore/QuartzCore.h>
|
#import "MWCommon.h"
|
#import "MWPhotoBrowser.h"
|
#import "MWPhotoBrowserPrivate.h"
|
#import "SDImageCache.h"
|
#import "UIImage+MWPhotoBrowser.h"
|
|
#define PADDING 10
|
|
static void * MWVideoPlayerObservation = &MWVideoPlayerObservation;
|
|
@implementation MWPhotoBrowser
|
|
#pragma mark - Init
|
|
- (id)init {
|
if ((self = [super init])) {
|
[self _initialisation];
|
}
|
return self;
|
}
|
|
- (id)initWithDelegate:(id <MWPhotoBrowserDelegate>)delegate {
|
if ((self = [self init])) {
|
_delegate = delegate;
|
}
|
return self;
|
}
|
|
- (id)initWithPhotos:(NSArray *)photosArray {
|
if ((self = [self init])) {
|
_fixedPhotosArray = photosArray;
|
}
|
return self;
|
}
|
|
- (id)initWithCoder:(NSCoder *)decoder {
|
if ((self = [super initWithCoder:decoder])) {
|
[self _initialisation];
|
}
|
return self;
|
}
|
|
- (void)_initialisation {
|
|
// Defaults
|
NSNumber *isVCBasedStatusBarAppearanceNum = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
|
if (isVCBasedStatusBarAppearanceNum) {
|
_isVCBasedStatusBarAppearance = isVCBasedStatusBarAppearanceNum.boolValue;
|
} else {
|
_isVCBasedStatusBarAppearance = YES; // default
|
}
|
self.hidesBottomBarWhenPushed = YES;
|
_hasBelongedToViewController = NO;
|
_photoCount = NSNotFound;
|
_previousLayoutBounds = CGRectZero;
|
_currentPageIndex = 0;
|
_previousPageIndex = NSUIntegerMax;
|
_displayActionButton = YES;
|
_displayNavArrows = NO;
|
_zoomPhotosToFill = YES;
|
_performingLayout = NO; // Reset on view did appear
|
_rotating = NO;
|
_viewIsActive = NO;
|
_enableGrid = YES;
|
_startOnGrid = NO;
|
_enableSwipeToDismiss = YES;
|
_delayToHideElements = 5;
|
_visiblePages = [[NSMutableSet alloc] init];
|
_recycledPages = [[NSMutableSet alloc] init];
|
_photos = [[NSMutableArray alloc] init];
|
_thumbPhotos = [[NSMutableArray alloc] init];
|
_currentGridContentOffset = CGPointMake(0, CGFLOAT_MAX);
|
_didSavePreviousStateOfNavBar = NO;
|
self.automaticallyAdjustsScrollViewInsets = NO;
|
|
// Listen for MWPhoto notifications
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
selector:@selector(handleMWPhotoLoadingDidEndNotification:)
|
name:MWPHOTO_LOADING_DID_END_NOTIFICATION
|
object:nil];
|
|
}
|
|
- (void)dealloc {
|
[self clearCurrentVideo];
|
_pagingScrollView.delegate = nil;
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
[self releaseAllUnderlyingPhotos:NO];
|
[[SDImageCache sharedImageCache] clearMemory]; // clear memory
|
}
|
|
- (void)releaseAllUnderlyingPhotos:(BOOL)preserveCurrent {
|
// Create a copy in case this array is modified while we are looping through
|
// Release photos
|
NSArray *copy = [_photos copy];
|
for (id p in copy) {
|
if (p != [NSNull null]) {
|
if (preserveCurrent && p == [self photoAtIndex:self.currentIndex]) {
|
continue; // skip current
|
}
|
[p unloadUnderlyingImage];
|
}
|
}
|
// Release thumbs
|
copy = [_thumbPhotos copy];
|
for (id p in copy) {
|
if (p != [NSNull null]) {
|
[p unloadUnderlyingImage];
|
}
|
}
|
}
|
|
- (void)didReceiveMemoryWarning {
|
|
// Release any cached data, images, etc that aren't in use.
|
[self releaseAllUnderlyingPhotos:YES];
|
[_recycledPages removeAllObjects];
|
|
// Releases the view if it doesn't have a superview.
|
[super didReceiveMemoryWarning];
|
|
}
|
|
#pragma mark - View Loading
|
|
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
|
- (void)viewDidLoad {
|
|
// Validate grid settings
|
if (_startOnGrid) _enableGrid = YES;
|
if (_enableGrid) {
|
_enableGrid = [_delegate respondsToSelector:@selector(photoBrowser:thumbPhotoAtIndex:)];
|
}
|
if (!_enableGrid) _startOnGrid = NO;
|
|
// View
|
self.view.backgroundColor = [UIColor blackColor];
|
self.view.clipsToBounds = YES;
|
|
// Setup paging scrolling view
|
CGRect pagingScrollViewFrame = [self frameForPagingScrollView];
|
_pagingScrollView = [[UIScrollView alloc] initWithFrame:pagingScrollViewFrame];
|
_pagingScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
_pagingScrollView.pagingEnabled = YES;
|
_pagingScrollView.delegate = self;
|
_pagingScrollView.showsHorizontalScrollIndicator = NO;
|
_pagingScrollView.showsVerticalScrollIndicator = NO;
|
_pagingScrollView.backgroundColor = [UIColor blackColor];
|
_pagingScrollView.contentSize = [self contentSizeForPagingScrollView];
|
[self.view addSubview:_pagingScrollView];
|
|
// Toolbar
|
_toolbar = [[UIToolbar alloc] initWithFrame:[self frameForToolbarAtOrientation:self.interfaceOrientation]];
|
_toolbar.tintColor = [UIColor whiteColor];
|
_toolbar.barTintColor = nil;
|
[_toolbar setBackgroundImage:nil forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsDefault];
|
[_toolbar setBackgroundImage:nil forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsLandscapePhone];
|
_toolbar.barStyle = UIBarStyleBlackTranslucent;
|
_toolbar.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
|
|
// Toolbar Items
|
if (self.displayNavArrows) {
|
NSString *arrowPathFormat = @"MWPhotoBrowser.bundle/UIBarButtonItemArrow%@";
|
UIImage *previousButtonImage = [UIImage imageForResourcePath:[NSString stringWithFormat:arrowPathFormat, @"Left"] ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]];
|
UIImage *nextButtonImage = [UIImage imageForResourcePath:[NSString stringWithFormat:arrowPathFormat, @"Right"] ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]];
|
_previousButton = [[UIBarButtonItem alloc] initWithImage:previousButtonImage style:UIBarButtonItemStylePlain target:self action:@selector(gotoPreviousPage)];
|
_nextButton = [[UIBarButtonItem alloc] initWithImage:nextButtonImage style:UIBarButtonItemStylePlain target:self action:@selector(gotoNextPage)];
|
}
|
if (self.displayActionButton) {
|
_actionButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(actionButtonPressed:)];
|
}
|
|
// Update
|
[self reloadData];
|
|
// Swipe to dismiss
|
if (_enableSwipeToDismiss) {
|
UISwipeGestureRecognizer *swipeGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonPressed:)];
|
swipeGesture.direction = UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionUp;
|
[self.view addGestureRecognizer:swipeGesture];
|
}
|
|
// Super
|
[super viewDidLoad];
|
|
}
|
|
- (void)performLayout {
|
|
// Setup
|
_performingLayout = YES;
|
NSUInteger numberOfPhotos = [self numberOfPhotos];
|
|
// Setup pages
|
[_visiblePages removeAllObjects];
|
[_recycledPages removeAllObjects];
|
|
// Navigation buttons
|
if ([self.navigationController.viewControllers objectAtIndex:0] == self) {
|
// We're first on stack so show done button
|
_doneButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Done", nil) style:UIBarButtonItemStylePlain target:self action:@selector(doneButtonPressed:)];
|
// Set appearance
|
[_doneButton setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
[_doneButton setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
[_doneButton setBackgroundImage:nil forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
|
[_doneButton setBackgroundImage:nil forState:UIControlStateHighlighted barMetrics:UIBarMetricsLandscapePhone];
|
[_doneButton setTitleTextAttributes:[NSDictionary dictionary] forState:UIControlStateNormal];
|
[_doneButton setTitleTextAttributes:[NSDictionary dictionary] forState:UIControlStateHighlighted];
|
self.navigationItem.rightBarButtonItem = _doneButton;
|
} else {
|
// We're not first so show back button
|
UIViewController *previousViewController = [self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2];
|
NSString *backButtonTitle = previousViewController.navigationItem.backBarButtonItem ? previousViewController.navigationItem.backBarButtonItem.title : previousViewController.title;
|
UIBarButtonItem *newBackButton = [[UIBarButtonItem alloc] initWithTitle:backButtonTitle style:UIBarButtonItemStylePlain target:nil action:nil];
|
// Appearance
|
[newBackButton setBackButtonBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
[newBackButton setBackButtonBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
[newBackButton setBackButtonBackgroundImage:nil forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
|
[newBackButton setBackButtonBackgroundImage:nil forState:UIControlStateHighlighted barMetrics:UIBarMetricsLandscapePhone];
|
[newBackButton setTitleTextAttributes:[NSDictionary dictionary] forState:UIControlStateNormal];
|
[newBackButton setTitleTextAttributes:[NSDictionary dictionary] forState:UIControlStateHighlighted];
|
_previousViewControllerBackButton = previousViewController.navigationItem.backBarButtonItem; // remember previous
|
previousViewController.navigationItem.backBarButtonItem = newBackButton;
|
}
|
|
// Toolbar items
|
BOOL hasItems = NO;
|
UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:self action:nil];
|
fixedSpace.width = 32; // To balance action button
|
UIBarButtonItem *flexSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:self action:nil];
|
NSMutableArray *items = [[NSMutableArray alloc] init];
|
|
// Left button - Grid
|
if (_enableGrid) {
|
hasItems = YES;
|
[items addObject:[[UIBarButtonItem alloc] initWithImage:[UIImage imageForResourcePath:@"MWPhotoBrowser.bundle/UIBarButtonItemGrid" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]] style:UIBarButtonItemStylePlain target:self action:@selector(showGridAnimated)]];
|
} else {
|
[items addObject:fixedSpace];
|
}
|
|
// Middle - Nav
|
if (_previousButton && _nextButton && numberOfPhotos > 1) {
|
hasItems = YES;
|
[items addObject:flexSpace];
|
[items addObject:_previousButton];
|
[items addObject:flexSpace];
|
[items addObject:_nextButton];
|
[items addObject:flexSpace];
|
} else {
|
[items addObject:flexSpace];
|
}
|
|
// Right - Action
|
if (_actionButton && !(!hasItems && !self.navigationItem.rightBarButtonItem)) {
|
[items addObject:_actionButton];
|
} else {
|
// We're not showing the toolbar so try and show in top right
|
if (_actionButton)
|
self.navigationItem.rightBarButtonItem = _actionButton;
|
[items addObject:fixedSpace];
|
}
|
|
// Toolbar visibility
|
[_toolbar setItems:items];
|
BOOL hideToolbar = YES;
|
for (UIBarButtonItem* item in _toolbar.items) {
|
if (item != fixedSpace && item != flexSpace) {
|
hideToolbar = NO;
|
break;
|
}
|
}
|
if (hideToolbar) {
|
[_toolbar removeFromSuperview];
|
} else {
|
[self.view addSubview:_toolbar];
|
}
|
|
// Update nav
|
[self updateNavigation];
|
|
// Content offset
|
_pagingScrollView.contentOffset = [self contentOffsetForPageAtIndex:_currentPageIndex];
|
[self tilePages];
|
_performingLayout = NO;
|
|
}
|
|
// Release any retained subviews of the main view.
|
- (void)viewDidUnload {
|
_currentPageIndex = 0;
|
_pagingScrollView = nil;
|
_visiblePages = nil;
|
_recycledPages = nil;
|
_toolbar = nil;
|
_previousButton = nil;
|
_nextButton = nil;
|
_progressHUD = nil;
|
[super viewDidUnload];
|
}
|
|
- (BOOL)presentingViewControllerPrefersStatusBarHidden {
|
UIViewController *presenting = self.presentingViewController;
|
if (presenting) {
|
if ([presenting isKindOfClass:[UINavigationController class]]) {
|
presenting = [(UINavigationController *)presenting topViewController];
|
}
|
} else {
|
// We're in a navigation controller so get previous one!
|
if (self.navigationController && self.navigationController.viewControllers.count > 1) {
|
presenting = [self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2];
|
}
|
}
|
if (presenting) {
|
return [presenting prefersStatusBarHidden];
|
} else {
|
return NO;
|
}
|
}
|
|
#pragma mark - Appearance
|
|
- (void)viewWillAppear:(BOOL)animated {
|
|
// Super
|
[super viewWillAppear:animated];
|
|
// Status bar
|
if (!_viewHasAppearedInitially) {
|
_leaveStatusBarAlone = [self presentingViewControllerPrefersStatusBarHidden];
|
// Check if status bar is hidden on first appear, and if so then ignore it
|
if (CGRectEqualToRect([[UIApplication sharedApplication] statusBarFrame], CGRectZero)) {
|
_leaveStatusBarAlone = YES;
|
}
|
}
|
// Set style
|
if (!_leaveStatusBarAlone && UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
|
_previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
|
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent animated:animated];
|
}
|
|
// Navigation bar appearance
|
if (!_viewIsActive && [self.navigationController.viewControllers objectAtIndex:0] != self) {
|
[self storePreviousNavBarAppearance];
|
}
|
[self setNavBarAppearance:animated];
|
|
// Update UI
|
[self hideControlsAfterDelay];
|
|
// Initial appearance
|
if (!_viewHasAppearedInitially) {
|
if (_startOnGrid) {
|
[self showGrid:NO];
|
}
|
}
|
|
// If rotation occured while we're presenting a modal
|
// and the index changed, make sure we show the right one now
|
if (_currentPageIndex != _pageIndexBeforeRotation) {
|
[self jumpToPageAtIndex:_pageIndexBeforeRotation animated:NO];
|
}
|
|
}
|
|
- (void)viewDidAppear:(BOOL)animated {
|
[super viewDidAppear:animated];
|
_viewIsActive = YES;
|
|
// Autoplay if first is video
|
if (!_viewHasAppearedInitially) {
|
if (_autoPlayOnAppear) {
|
MWPhoto *photo = [self photoAtIndex:_currentPageIndex];
|
if ([photo respondsToSelector:@selector(isVideo)] && photo.isVideo) {
|
[self playVideoAtIndex:_currentPageIndex];
|
}
|
}
|
}
|
|
_viewHasAppearedInitially = YES;
|
|
}
|
|
- (void)viewWillDisappear:(BOOL)animated {
|
|
// Detect if rotation occurs while we're presenting a modal
|
_pageIndexBeforeRotation = _currentPageIndex;
|
|
// Check that we're being popped for good
|
if ([self.navigationController.viewControllers objectAtIndex:0] != self &&
|
![self.navigationController.viewControllers containsObject:self]) {
|
|
// State
|
_viewIsActive = NO;
|
|
// Bar state / appearance
|
[self restorePreviousNavBarAppearance:animated];
|
|
}
|
|
// Controls
|
[self.navigationController.navigationBar.layer removeAllAnimations]; // Stop all animations on nav bar
|
[NSObject cancelPreviousPerformRequestsWithTarget:self]; // Cancel any pending toggles from taps
|
[self setControlsHidden:NO animated:NO permanent:YES];
|
|
// Status bar
|
if (!_leaveStatusBarAlone && UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
|
[[UIApplication sharedApplication] setStatusBarStyle:_previousStatusBarStyle animated:animated];
|
}
|
|
// Super
|
[super viewWillDisappear:animated];
|
|
}
|
|
- (void)willMoveToParentViewController:(UIViewController *)parent {
|
if (parent && _hasBelongedToViewController) {
|
[NSException raise:@"MWPhotoBrowser Instance Reuse" format:@"MWPhotoBrowser instances cannot be reused."];
|
}
|
}
|
|
- (void)didMoveToParentViewController:(UIViewController *)parent {
|
if (!parent) _hasBelongedToViewController = YES;
|
}
|
|
#pragma mark - Nav Bar Appearance
|
|
- (void)setNavBarAppearance:(BOOL)animated {
|
[self.navigationController setNavigationBarHidden:NO animated:animated];
|
UINavigationBar *navBar = self.navigationController.navigationBar;
|
navBar.tintColor = [UIColor whiteColor];
|
navBar.barTintColor = nil;
|
navBar.shadowImage = nil;
|
navBar.translucent = YES;
|
navBar.barStyle = UIBarStyleBlackTranslucent;
|
[navBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault];
|
[navBar setBackgroundImage:nil forBarMetrics:UIBarMetricsLandscapePhone];
|
}
|
|
- (void)storePreviousNavBarAppearance {
|
_didSavePreviousStateOfNavBar = YES;
|
_previousNavBarBarTintColor = self.navigationController.navigationBar.barTintColor;
|
_previousNavBarTranslucent = self.navigationController.navigationBar.translucent;
|
_previousNavBarTintColor = self.navigationController.navigationBar.tintColor;
|
_previousNavBarHidden = self.navigationController.navigationBarHidden;
|
_previousNavBarStyle = self.navigationController.navigationBar.barStyle;
|
_previousNavigationBarBackgroundImageDefault = [self.navigationController.navigationBar backgroundImageForBarMetrics:UIBarMetricsDefault];
|
_previousNavigationBarBackgroundImageLandscapePhone = [self.navigationController.navigationBar backgroundImageForBarMetrics:UIBarMetricsLandscapePhone];
|
}
|
|
- (void)restorePreviousNavBarAppearance:(BOOL)animated {
|
if (_didSavePreviousStateOfNavBar) {
|
[self.navigationController setNavigationBarHidden:_previousNavBarHidden animated:animated];
|
UINavigationBar *navBar = self.navigationController.navigationBar;
|
navBar.tintColor = _previousNavBarTintColor;
|
navBar.translucent = _previousNavBarTranslucent;
|
navBar.barTintColor = _previousNavBarBarTintColor;
|
navBar.barStyle = _previousNavBarStyle;
|
[navBar setBackgroundImage:_previousNavigationBarBackgroundImageDefault forBarMetrics:UIBarMetricsDefault];
|
[navBar setBackgroundImage:_previousNavigationBarBackgroundImageLandscapePhone forBarMetrics:UIBarMetricsLandscapePhone];
|
// Restore back button if we need to
|
if (_previousViewControllerBackButton) {
|
UIViewController *previousViewController = [self.navigationController topViewController]; // We've disappeared so previous is now top
|
previousViewController.navigationItem.backBarButtonItem = _previousViewControllerBackButton;
|
_previousViewControllerBackButton = nil;
|
}
|
}
|
}
|
|
#pragma mark - Layout
|
|
- (void)viewWillLayoutSubviews {
|
[super viewWillLayoutSubviews];
|
[self layoutVisiblePages];
|
}
|
|
- (void)layoutVisiblePages {
|
|
// Flag
|
_performingLayout = YES;
|
|
// Toolbar
|
_toolbar.frame = [self frameForToolbarAtOrientation:self.interfaceOrientation];
|
|
// Remember index
|
NSUInteger indexPriorToLayout = _currentPageIndex;
|
|
// Get paging scroll view frame to determine if anything needs changing
|
CGRect pagingScrollViewFrame = [self frameForPagingScrollView];
|
|
// Frame needs changing
|
if (!_skipNextPagingScrollViewPositioning) {
|
_pagingScrollView.frame = pagingScrollViewFrame;
|
}
|
_skipNextPagingScrollViewPositioning = NO;
|
|
// Recalculate contentSize based on current orientation
|
_pagingScrollView.contentSize = [self contentSizeForPagingScrollView];
|
|
// Adjust frames and configuration of each visible page
|
for (MWZoomingScrollView *page in _visiblePages) {
|
NSUInteger index = page.index;
|
page.frame = [self frameForPageAtIndex:index];
|
if (page.captionView) {
|
page.captionView.frame = [self frameForCaptionView:page.captionView atIndex:index];
|
}
|
if (page.selectedButton) {
|
page.selectedButton.frame = [self frameForSelectedButton:page.selectedButton atIndex:index];
|
}
|
if (page.playButton) {
|
page.playButton.frame = [self frameForPlayButton:page.playButton atIndex:index];
|
}
|
|
// Adjust scales if bounds has changed since last time
|
if (!CGRectEqualToRect(_previousLayoutBounds, self.view.bounds)) {
|
// Update zooms for new bounds
|
[page setMaxMinZoomScalesForCurrentBounds];
|
_previousLayoutBounds = self.view.bounds;
|
}
|
|
}
|
|
// Adjust video loading indicator if it's visible
|
[self positionVideoLoadingIndicator];
|
|
// Adjust contentOffset to preserve page location based on values collected prior to location
|
_pagingScrollView.contentOffset = [self contentOffsetForPageAtIndex:indexPriorToLayout];
|
[self didStartViewingPageAtIndex:_currentPageIndex]; // initial
|
|
// Reset
|
_currentPageIndex = indexPriorToLayout;
|
_performingLayout = NO;
|
|
}
|
|
#pragma mark - Rotation
|
|
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
|
return YES;
|
}
|
|
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
return UIInterfaceOrientationMaskAll;
|
}
|
|
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
|
|
// Remember page index before rotation
|
_pageIndexBeforeRotation = _currentPageIndex;
|
_rotating = YES;
|
|
// In iOS 7 the nav bar gets shown after rotation, but might as well do this for everything!
|
if ([self areControlsHidden]) {
|
// Force hidden
|
self.navigationController.navigationBarHidden = YES;
|
}
|
|
}
|
|
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
|
|
// Perform layout
|
_currentPageIndex = _pageIndexBeforeRotation;
|
|
// Delay control holding
|
[self hideControlsAfterDelay];
|
|
// Layout
|
[self layoutVisiblePages];
|
|
}
|
|
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
|
_rotating = NO;
|
// Ensure nav bar isn't re-displayed
|
if ([self areControlsHidden]) {
|
self.navigationController.navigationBarHidden = NO;
|
self.navigationController.navigationBar.alpha = 0;
|
}
|
}
|
|
#pragma mark - Data
|
|
- (NSUInteger)currentIndex {
|
return _currentPageIndex;
|
}
|
|
- (void)reloadData {
|
|
// Reset
|
_photoCount = NSNotFound;
|
|
// Get data
|
NSUInteger numberOfPhotos = [self numberOfPhotos];
|
[self releaseAllUnderlyingPhotos:YES];
|
[_photos removeAllObjects];
|
[_thumbPhotos removeAllObjects];
|
for (int i = 0; i < numberOfPhotos; i++) {
|
[_photos addObject:[NSNull null]];
|
[_thumbPhotos addObject:[NSNull null]];
|
}
|
|
// Update current page index
|
if (numberOfPhotos > 0) {
|
_currentPageIndex = MAX(0, MIN(_currentPageIndex, numberOfPhotos - 1));
|
} else {
|
_currentPageIndex = 0;
|
}
|
|
// Update layout
|
if ([self isViewLoaded]) {
|
while (_pagingScrollView.subviews.count) {
|
[[_pagingScrollView.subviews lastObject] removeFromSuperview];
|
}
|
[self performLayout];
|
[self.view setNeedsLayout];
|
}
|
|
}
|
|
- (NSUInteger)numberOfPhotos {
|
if (_photoCount == NSNotFound) {
|
if ([_delegate respondsToSelector:@selector(numberOfPhotosInPhotoBrowser:)]) {
|
_photoCount = [_delegate numberOfPhotosInPhotoBrowser:self];
|
} else if (_fixedPhotosArray) {
|
_photoCount = _fixedPhotosArray.count;
|
}
|
}
|
if (_photoCount == NSNotFound) _photoCount = 0;
|
return _photoCount;
|
}
|
|
- (id<MWPhoto>)photoAtIndex:(NSUInteger)index {
|
id <MWPhoto> photo = nil;
|
if (index < _photos.count) {
|
if ([_photos objectAtIndex:index] == [NSNull null]) {
|
if ([_delegate respondsToSelector:@selector(photoBrowser:photoAtIndex:)]) {
|
photo = [_delegate photoBrowser:self photoAtIndex:index];
|
} else if (_fixedPhotosArray && index < _fixedPhotosArray.count) {
|
photo = [_fixedPhotosArray objectAtIndex:index];
|
}
|
if (photo) [_photos replaceObjectAtIndex:index withObject:photo];
|
} else {
|
photo = [_photos objectAtIndex:index];
|
}
|
}
|
return photo;
|
}
|
|
- (id<MWPhoto>)thumbPhotoAtIndex:(NSUInteger)index {
|
id <MWPhoto> photo = nil;
|
if (index < _thumbPhotos.count) {
|
if ([_thumbPhotos objectAtIndex:index] == [NSNull null]) {
|
if ([_delegate respondsToSelector:@selector(photoBrowser:thumbPhotoAtIndex:)]) {
|
photo = [_delegate photoBrowser:self thumbPhotoAtIndex:index];
|
}
|
if (photo) [_thumbPhotos replaceObjectAtIndex:index withObject:photo];
|
} else {
|
photo = [_thumbPhotos objectAtIndex:index];
|
}
|
}
|
return photo;
|
}
|
|
- (MWCaptionView *)captionViewForPhotoAtIndex:(NSUInteger)index {
|
MWCaptionView *captionView = nil;
|
if ([_delegate respondsToSelector:@selector(photoBrowser:captionViewForPhotoAtIndex:)]) {
|
captionView = [_delegate photoBrowser:self captionViewForPhotoAtIndex:index];
|
} else {
|
id <MWPhoto> photo = [self photoAtIndex:index];
|
if ([photo respondsToSelector:@selector(caption)]) {
|
if ([photo caption]) captionView = [[MWCaptionView alloc] initWithPhoto:photo];
|
}
|
}
|
captionView.alpha = [self areControlsHidden] ? 0 : 1; // Initial alpha
|
return captionView;
|
}
|
|
- (BOOL)photoIsSelectedAtIndex:(NSUInteger)index {
|
BOOL value = NO;
|
if (_displaySelectionButtons) {
|
if ([self.delegate respondsToSelector:@selector(photoBrowser:isPhotoSelectedAtIndex:)]) {
|
value = [self.delegate photoBrowser:self isPhotoSelectedAtIndex:index];
|
}
|
}
|
return value;
|
}
|
|
- (void)setPhotoSelected:(BOOL)selected atIndex:(NSUInteger)index {
|
if (_displaySelectionButtons) {
|
if ([self.delegate respondsToSelector:@selector(photoBrowser:photoAtIndex:selectedChanged:)]) {
|
[self.delegate photoBrowser:self photoAtIndex:index selectedChanged:selected];
|
}
|
}
|
}
|
|
- (UIImage *)imageForPhoto:(id<MWPhoto>)photo {
|
if (photo) {
|
// Get image or obtain in background
|
if ([photo underlyingImage]) {
|
return [photo underlyingImage];
|
} else {
|
[photo loadUnderlyingImageAndNotify];
|
}
|
}
|
return nil;
|
}
|
|
- (void)loadAdjacentPhotosIfNecessary:(id<MWPhoto>)photo {
|
MWZoomingScrollView *page = [self pageDisplayingPhoto:photo];
|
if (page) {
|
// If page is current page then initiate loading of previous and next pages
|
NSUInteger pageIndex = page.index;
|
if (_currentPageIndex == pageIndex) {
|
if (pageIndex > 0) {
|
// Preload index - 1
|
id <MWPhoto> photo = [self photoAtIndex:pageIndex-1];
|
if (![photo underlyingImage]) {
|
[photo loadUnderlyingImageAndNotify];
|
MWLog(@"Pre-loading image at index %lu", (unsigned long)pageIndex-1);
|
}
|
}
|
if (pageIndex < [self numberOfPhotos] - 1) {
|
// Preload index + 1
|
id <MWPhoto> photo = [self photoAtIndex:pageIndex+1];
|
if (![photo underlyingImage]) {
|
[photo loadUnderlyingImageAndNotify];
|
MWLog(@"Pre-loading image at index %lu", (unsigned long)pageIndex+1);
|
}
|
}
|
}
|
}
|
}
|
|
#pragma mark - MWPhoto Loading Notification
|
|
- (void)handleMWPhotoLoadingDidEndNotification:(NSNotification *)notification {
|
id <MWPhoto> photo = [notification object];
|
MWZoomingScrollView *page = [self pageDisplayingPhoto:photo];
|
if (page) {
|
if ([photo underlyingImage]) {
|
// Successful load
|
[page displayImage];
|
[self loadAdjacentPhotosIfNecessary:photo];
|
} else {
|
|
// Failed to load
|
[page displayImageFailure];
|
}
|
// Update nav
|
[self updateNavigation];
|
}
|
}
|
|
#pragma mark - Paging
|
|
- (void)tilePages {
|
|
// Calculate which pages should be visible
|
// Ignore padding as paging bounces encroach on that
|
// and lead to false page loads
|
CGRect visibleBounds = _pagingScrollView.bounds;
|
NSInteger iFirstIndex = (NSInteger)floorf((CGRectGetMinX(visibleBounds)+PADDING*2) / CGRectGetWidth(visibleBounds));
|
NSInteger iLastIndex = (NSInteger)floorf((CGRectGetMaxX(visibleBounds)-PADDING*2-1) / CGRectGetWidth(visibleBounds));
|
if (iFirstIndex < 0) iFirstIndex = 0;
|
if (iFirstIndex > [self numberOfPhotos] - 1) iFirstIndex = [self numberOfPhotos] - 1;
|
if (iLastIndex < 0) iLastIndex = 0;
|
if (iLastIndex > [self numberOfPhotos] - 1) iLastIndex = [self numberOfPhotos] - 1;
|
|
// Recycle no longer needed pages
|
NSInteger pageIndex;
|
for (MWZoomingScrollView *page in _visiblePages) {
|
pageIndex = page.index;
|
if (pageIndex < (NSUInteger)iFirstIndex || pageIndex > (NSUInteger)iLastIndex) {
|
[_recycledPages addObject:page];
|
[page.captionView removeFromSuperview];
|
[page.selectedButton removeFromSuperview];
|
[page.playButton removeFromSuperview];
|
[page prepareForReuse];
|
[page removeFromSuperview];
|
MWLog(@"Removed page at index %lu", (unsigned long)pageIndex);
|
}
|
}
|
[_visiblePages minusSet:_recycledPages];
|
while (_recycledPages.count > 2) // Only keep 2 recycled pages
|
[_recycledPages removeObject:[_recycledPages anyObject]];
|
|
// Add missing pages
|
for (NSUInteger index = (NSUInteger)iFirstIndex; index <= (NSUInteger)iLastIndex; index++) {
|
if (![self isDisplayingPageForIndex:index]) {
|
|
// Add new page
|
MWZoomingScrollView *page = [self dequeueRecycledPage];
|
if (!page) {
|
page = [[MWZoomingScrollView alloc] initWithPhotoBrowser:self];
|
}
|
[_visiblePages addObject:page];
|
[self configurePage:page forIndex:index];
|
|
[_pagingScrollView addSubview:page];
|
MWLog(@"Added page at index %lu", (unsigned long)index);
|
|
// Add caption
|
MWCaptionView *captionView = [self captionViewForPhotoAtIndex:index];
|
if (captionView) {
|
captionView.frame = [self frameForCaptionView:captionView atIndex:index];
|
[_pagingScrollView addSubview:captionView];
|
page.captionView = captionView;
|
}
|
|
// Add play button if needed
|
if (page.displayingVideo) {
|
UIButton *playButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
[playButton setImage:[UIImage imageForResourcePath:@"MWPhotoBrowser.bundle/PlayButtonOverlayLarge" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]] forState:UIControlStateNormal];
|
[playButton setImage:[UIImage imageForResourcePath:@"MWPhotoBrowser.bundle/PlayButtonOverlayLargeTap" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]] forState:UIControlStateHighlighted];
|
[playButton addTarget:self action:@selector(playButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
[playButton sizeToFit];
|
playButton.frame = [self frameForPlayButton:playButton atIndex:index];
|
[_pagingScrollView addSubview:playButton];
|
page.playButton = playButton;
|
}
|
|
// Add selected button
|
if (self.displaySelectionButtons) {
|
UIButton *selectedButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
[selectedButton setImage:[UIImage imageForResourcePath:@"MWPhotoBrowser.bundle/ImageSelectedOff" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]] forState:UIControlStateNormal];
|
UIImage *selectedOnImage;
|
if (self.customImageSelectedIconName) {
|
selectedOnImage = [UIImage imageNamed:self.customImageSelectedIconName];
|
} else {
|
selectedOnImage = [UIImage imageForResourcePath:@"MWPhotoBrowser.bundle/ImageSelectedOn" ofType:@"png" inBundle:[NSBundle bundleForClass:[self class]]];
|
}
|
[selectedButton setImage:selectedOnImage forState:UIControlStateSelected];
|
[selectedButton sizeToFit];
|
selectedButton.adjustsImageWhenHighlighted = NO;
|
[selectedButton addTarget:self action:@selector(selectedButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
selectedButton.frame = [self frameForSelectedButton:selectedButton atIndex:index];
|
[_pagingScrollView addSubview:selectedButton];
|
page.selectedButton = selectedButton;
|
selectedButton.selected = [self photoIsSelectedAtIndex:index];
|
}
|
|
}
|
}
|
|
}
|
|
- (void)updateVisiblePageStates {
|
NSSet *copy = [_visiblePages copy];
|
for (MWZoomingScrollView *page in copy) {
|
|
// Update selection
|
page.selectedButton.selected = [self photoIsSelectedAtIndex:page.index];
|
|
}
|
}
|
|
- (BOOL)isDisplayingPageForIndex:(NSUInteger)index {
|
for (MWZoomingScrollView *page in _visiblePages)
|
if (page.index == index) return YES;
|
return NO;
|
}
|
|
- (MWZoomingScrollView *)pageDisplayedAtIndex:(NSUInteger)index {
|
MWZoomingScrollView *thePage = nil;
|
for (MWZoomingScrollView *page in _visiblePages) {
|
if (page.index == index) {
|
thePage = page; break;
|
}
|
}
|
return thePage;
|
}
|
|
- (MWZoomingScrollView *)pageDisplayingPhoto:(id<MWPhoto>)photo {
|
MWZoomingScrollView *thePage = nil;
|
for (MWZoomingScrollView *page in _visiblePages) {
|
if (page.photo == photo) {
|
thePage = page; break;
|
}
|
}
|
return thePage;
|
}
|
|
- (void)configurePage:(MWZoomingScrollView *)page forIndex:(NSUInteger)index {
|
page.frame = [self frameForPageAtIndex:index];
|
page.index = index;
|
page.photo = [self photoAtIndex:index];
|
}
|
|
- (MWZoomingScrollView *)dequeueRecycledPage {
|
MWZoomingScrollView *page = [_recycledPages anyObject];
|
if (page) {
|
[_recycledPages removeObject:page];
|
}
|
return page;
|
}
|
|
// Handle page changes
|
- (void)didStartViewingPageAtIndex:(NSUInteger)index {
|
|
// Handle 0 photos
|
if (![self numberOfPhotos]) {
|
// Show controls
|
[self setControlsHidden:NO animated:YES permanent:YES];
|
return;
|
}
|
|
// Handle video on page change
|
if (!_rotating || index != _currentVideoIndex) {
|
[self clearCurrentVideo];
|
}
|
|
// Release images further away than +/-1
|
NSUInteger i;
|
if (index > 0) {
|
// Release anything < index - 1
|
for (i = 0; i < index-1; i++) {
|
id photo = [_photos objectAtIndex:i];
|
if (photo != [NSNull null]) {
|
[photo unloadUnderlyingImage];
|
[_photos replaceObjectAtIndex:i withObject:[NSNull null]];
|
MWLog(@"Released underlying image at index %lu", (unsigned long)i);
|
}
|
}
|
}
|
if (index < [self numberOfPhotos] - 1) {
|
// Release anything > index + 1
|
for (i = index + 2; i < _photos.count; i++) {
|
id photo = [_photos objectAtIndex:i];
|
if (photo != [NSNull null]) {
|
[photo unloadUnderlyingImage];
|
[_photos replaceObjectAtIndex:i withObject:[NSNull null]];
|
MWLog(@"Released underlying image at index %lu", (unsigned long)i);
|
}
|
}
|
}
|
|
// Load adjacent images if needed and the photo is already
|
// loaded. Also called after photo has been loaded in background
|
id <MWPhoto> currentPhoto = [self photoAtIndex:index];
|
if ([currentPhoto underlyingImage]) {
|
// photo loaded so load ajacent now
|
[self loadAdjacentPhotosIfNecessary:currentPhoto];
|
}
|
|
// Notify delegate
|
if (index != _previousPageIndex) {
|
if ([_delegate respondsToSelector:@selector(photoBrowser:didDisplayPhotoAtIndex:)])
|
[_delegate photoBrowser:self didDisplayPhotoAtIndex:index];
|
_previousPageIndex = index;
|
}
|
|
// Update nav
|
[self updateNavigation];
|
|
}
|
|
#pragma mark - Frame Calculations
|
|
- (CGRect)frameForPagingScrollView {
|
CGRect frame = self.view.bounds;// [[UIScreen mainScreen] bounds];
|
frame.origin.x -= PADDING;
|
frame.size.width += (2 * PADDING);
|
return CGRectIntegral(frame);
|
}
|
|
- (CGRect)frameForPageAtIndex:(NSUInteger)index {
|
// We have to use our paging scroll view's bounds, not frame, to calculate the page placement. When the device is in
|
// landscape orientation, the frame will still be in portrait because the pagingScrollView is the root view controller's
|
// view, so its frame is in window coordinate space, which is never rotated. Its bounds, however, will be in landscape
|
// because it has a rotation transform applied.
|
CGRect bounds = _pagingScrollView.bounds;
|
CGRect pageFrame = bounds;
|
pageFrame.size.width -= (2 * PADDING);
|
pageFrame.origin.x = (bounds.size.width * index) + PADDING;
|
return CGRectIntegral(pageFrame);
|
}
|
|
- (CGSize)contentSizeForPagingScrollView {
|
// We have to use the paging scroll view's bounds to calculate the contentSize, for the same reason outlined above.
|
CGRect bounds = _pagingScrollView.bounds;
|
return CGSizeMake(bounds.size.width * [self numberOfPhotos], bounds.size.height);
|
}
|
|
- (CGPoint)contentOffsetForPageAtIndex:(NSUInteger)index {
|
CGFloat pageWidth = _pagingScrollView.bounds.size.width;
|
CGFloat newOffset = index * pageWidth;
|
return CGPointMake(newOffset, 0);
|
}
|
|
- (CGRect)frameForToolbarAtOrientation:(UIInterfaceOrientation)orientation {
|
CGFloat height = 44;
|
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone &&
|
UIInterfaceOrientationIsLandscape(orientation)) height = 32;
|
return CGRectIntegral(CGRectMake(0, self.view.bounds.size.height - height, self.view.bounds.size.width, height));
|
}
|
|
- (CGRect)frameForCaptionView:(MWCaptionView *)captionView atIndex:(NSUInteger)index {
|
CGRect pageFrame = [self frameForPageAtIndex:index];
|
CGSize captionSize = [captionView sizeThatFits:CGSizeMake(pageFrame.size.width, 0)];
|
CGRect captionFrame = CGRectMake(pageFrame.origin.x,
|
pageFrame.size.height - captionSize.height - (_toolbar.superview?_toolbar.frame.size.height:0),
|
pageFrame.size.width,
|
captionSize.height);
|
return CGRectIntegral(captionFrame);
|
}
|
|
- (CGRect)frameForSelectedButton:(UIButton *)selectedButton atIndex:(NSUInteger)index {
|
CGRect pageFrame = [self frameForPageAtIndex:index];
|
CGFloat padding = 20;
|
CGFloat yOffset = 0;
|
if (![self areControlsHidden]) {
|
UINavigationBar *navBar = self.navigationController.navigationBar;
|
yOffset = navBar.frame.origin.y + navBar.frame.size.height;
|
}
|
CGRect selectedButtonFrame = CGRectMake(pageFrame.origin.x + pageFrame.size.width - selectedButton.frame.size.width - padding,
|
padding + yOffset,
|
selectedButton.frame.size.width,
|
selectedButton.frame.size.height);
|
return CGRectIntegral(selectedButtonFrame);
|
}
|
|
- (CGRect)frameForPlayButton:(UIButton *)playButton atIndex:(NSUInteger)index {
|
CGRect pageFrame = [self frameForPageAtIndex:index];
|
return CGRectMake(floorf(CGRectGetMidX(pageFrame) - playButton.frame.size.width / 2),
|
floorf(CGRectGetMidY(pageFrame) - playButton.frame.size.height / 2),
|
playButton.frame.size.width,
|
playButton.frame.size.height);
|
}
|
|
#pragma mark - UIScrollView Delegate
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
|
// Checks
|
if (!_viewIsActive || _performingLayout || _rotating) return;
|
|
// Tile pages
|
[self tilePages];
|
|
// Calculate current page
|
CGRect visibleBounds = _pagingScrollView.bounds;
|
NSInteger index = (NSInteger)(floorf(CGRectGetMidX(visibleBounds) / CGRectGetWidth(visibleBounds)));
|
if (index < 0) index = 0;
|
if (index > [self numberOfPhotos] - 1) index = [self numberOfPhotos] - 1;
|
NSUInteger previousCurrentPage = _currentPageIndex;
|
_currentPageIndex = index;
|
if (_currentPageIndex != previousCurrentPage) {
|
[self didStartViewingPageAtIndex:index];
|
}
|
|
}
|
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
// Hide controls when dragging begins
|
[self setControlsHidden:YES animated:YES permanent:NO];
|
}
|
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
// Update nav when page changes
|
[self updateNavigation];
|
}
|
|
#pragma mark - Navigation
|
|
- (void)updateNavigation {
|
|
// Title
|
NSUInteger numberOfPhotos = [self numberOfPhotos];
|
if (_gridController) {
|
if (_gridController.selectionMode) {
|
self.title = NSLocalizedString(@"Select Photos", nil);
|
} else {
|
NSString *photosText;
|
if (numberOfPhotos == 1) {
|
photosText = NSLocalizedString(@"photo", @"Used in the context: '1 photo'");
|
} else {
|
photosText = NSLocalizedString(@"photos", @"Used in the context: '3 photos'");
|
}
|
self.title = [NSString stringWithFormat:@"%lu %@", (unsigned long)numberOfPhotos, photosText];
|
}
|
} else if (numberOfPhotos > 1) {
|
if ([_delegate respondsToSelector:@selector(photoBrowser:titleForPhotoAtIndex:)]) {
|
self.title = [_delegate photoBrowser:self titleForPhotoAtIndex:_currentPageIndex];
|
} else {
|
self.title = [NSString stringWithFormat:@"%lu %@ %lu", (unsigned long)(_currentPageIndex+1), NSLocalizedString(@"of", @"Used in the context: 'Showing 1 of 3 items'"), (unsigned long)numberOfPhotos];
|
}
|
} else {
|
self.title = nil;
|
}
|
|
// Buttons
|
_previousButton.enabled = (_currentPageIndex > 0);
|
_nextButton.enabled = (_currentPageIndex < numberOfPhotos - 1);
|
|
// Disable action button if there is no image or it's a video
|
MWPhoto *photo = [self photoAtIndex:_currentPageIndex];
|
if ([photo underlyingImage] == nil || ([photo respondsToSelector:@selector(isVideo)] && photo.isVideo)) {
|
_actionButton.enabled = NO;
|
_actionButton.tintColor = [UIColor clearColor]; // Tint to hide button
|
} else {
|
_actionButton.enabled = YES;
|
_actionButton.tintColor = nil;
|
}
|
|
}
|
|
- (void)jumpToPageAtIndex:(NSUInteger)index animated:(BOOL)animated {
|
|
// Change page
|
if (index < [self numberOfPhotos]) {
|
CGRect pageFrame = [self frameForPageAtIndex:index];
|
[_pagingScrollView setContentOffset:CGPointMake(pageFrame.origin.x - PADDING, 0) animated:animated];
|
[self updateNavigation];
|
}
|
|
// Update timer to give more time
|
[self hideControlsAfterDelay];
|
|
}
|
|
- (void)gotoPreviousPage {
|
[self showPreviousPhotoAnimated:NO];
|
}
|
- (void)gotoNextPage {
|
[self showNextPhotoAnimated:NO];
|
}
|
|
- (void)showPreviousPhotoAnimated:(BOOL)animated {
|
[self jumpToPageAtIndex:_currentPageIndex-1 animated:animated];
|
}
|
|
- (void)showNextPhotoAnimated:(BOOL)animated {
|
[self jumpToPageAtIndex:_currentPageIndex+1 animated:animated];
|
}
|
|
#pragma mark - Interactions
|
|
- (void)selectedButtonTapped:(id)sender {
|
UIButton *selectedButton = (UIButton *)sender;
|
selectedButton.selected = !selectedButton.selected;
|
NSUInteger index = NSUIntegerMax;
|
for (MWZoomingScrollView *page in _visiblePages) {
|
if (page.selectedButton == selectedButton) {
|
index = page.index;
|
break;
|
}
|
}
|
if (index != NSUIntegerMax) {
|
[self setPhotoSelected:selectedButton.selected atIndex:index];
|
}
|
}
|
|
- (void)playButtonTapped:(id)sender {
|
UIButton *playButton = (UIButton *)sender;
|
NSUInteger index = NSUIntegerMax;
|
for (MWZoomingScrollView *page in _visiblePages) {
|
if (page.playButton == playButton) {
|
index = page.index;
|
break;
|
}
|
}
|
if (index != NSUIntegerMax) {
|
if (!_currentVideoPlayerViewController) {
|
[self playVideoAtIndex:index];
|
}
|
}
|
}
|
|
#pragma mark - Video
|
|
- (void)playVideoAtIndex:(NSUInteger)index {
|
id photo = [self photoAtIndex:index];
|
if ([photo respondsToSelector:@selector(getVideoURL:)]) {
|
|
// Valid for playing
|
_currentVideoIndex = index;
|
[self clearCurrentVideo];
|
[self setVideoLoadingIndicatorVisible:YES atPageIndex:index];
|
|
// Get video and play
|
[photo getVideoURL:^(NSURL *url) {
|
if (url) {
|
dispatch_async(dispatch_get_main_queue(), ^{
|
[self _playVideo:url atPhotoIndex:index];
|
});
|
} else {
|
[self setVideoLoadingIndicatorVisible:NO atPageIndex:index];
|
}
|
}];
|
|
}
|
}
|
|
- (void)_playVideo:(NSURL *)videoURL atPhotoIndex:(NSUInteger)index {
|
|
// Setup player
|
_currentVideoPlayerViewController = [[MPMoviePlayerViewController alloc] initWithContentURL:videoURL];
|
[_currentVideoPlayerViewController.moviePlayer prepareToPlay];
|
_currentVideoPlayerViewController.moviePlayer.shouldAutoplay = YES;
|
_currentVideoPlayerViewController.moviePlayer.scalingMode = MPMovieScalingModeAspectFit;
|
_currentVideoPlayerViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
|
|
// Remove the movie player view controller from the "playback did finish" notification observers
|
// Observe ourselves so we can get it to use the crossfade transition
|
[[NSNotificationCenter defaultCenter] removeObserver:_currentVideoPlayerViewController
|
name:MPMoviePlayerPlaybackDidFinishNotification
|
object:_currentVideoPlayerViewController.moviePlayer];
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
selector:@selector(videoFinishedCallback:)
|
name:MPMoviePlayerPlaybackDidFinishNotification
|
object:_currentVideoPlayerViewController.moviePlayer];
|
|
// Show
|
[self presentViewController:_currentVideoPlayerViewController animated:YES completion:nil];
|
|
}
|
|
- (void)videoFinishedCallback:(NSNotification*)notification {
|
|
// Remove observer
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
name:MPMoviePlayerPlaybackDidFinishNotification
|
object:_currentVideoPlayerViewController.moviePlayer];
|
|
// Clear up
|
[self clearCurrentVideo];
|
|
// Dismiss
|
BOOL error = [[[notification userInfo] objectForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] intValue] == MPMovieFinishReasonPlaybackError;
|
if (error) {
|
// Error occured so dismiss with a delay incase error was immediate and we need to wait to dismiss the VC
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
[self dismissViewControllerAnimated:YES completion:nil];
|
});
|
} else {
|
[self dismissViewControllerAnimated:YES completion:nil];
|
}
|
|
}
|
|
- (void)clearCurrentVideo {
|
if (!_currentVideoPlayerViewController) return;
|
[_currentVideoLoadingIndicator removeFromSuperview];
|
_currentVideoPlayerViewController = nil;
|
_currentVideoLoadingIndicator = nil;
|
_currentVideoIndex = NSUIntegerMax;
|
}
|
|
- (void)setVideoLoadingIndicatorVisible:(BOOL)visible atPageIndex:(NSUInteger)pageIndex {
|
if (_currentVideoLoadingIndicator && !visible) {
|
[_currentVideoLoadingIndicator removeFromSuperview];
|
_currentVideoLoadingIndicator = nil;
|
} else if (!_currentVideoLoadingIndicator && visible) {
|
_currentVideoLoadingIndicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectZero];
|
[_currentVideoLoadingIndicator sizeToFit];
|
[_currentVideoLoadingIndicator startAnimating];
|
[_pagingScrollView addSubview:_currentVideoLoadingIndicator];
|
[self positionVideoLoadingIndicator];
|
}
|
}
|
|
- (void)positionVideoLoadingIndicator {
|
if (_currentVideoLoadingIndicator && _currentVideoIndex != NSUIntegerMax) {
|
CGRect frame = [self frameForPageAtIndex:_currentVideoIndex];
|
_currentVideoLoadingIndicator.center = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
|
}
|
}
|
|
#pragma mark - Grid
|
|
- (void)showGridAnimated {
|
[self showGrid:YES];
|
}
|
|
- (void)showGrid:(BOOL)animated {
|
|
if (_gridController) return;
|
|
// Init grid controller
|
_gridController = [[MWGridViewController alloc] init];
|
_gridController.initialContentOffset = _currentGridContentOffset;
|
_gridController.browser = self;
|
_gridController.selectionMode = _displaySelectionButtons;
|
_gridController.view.frame = self.view.bounds;
|
_gridController.view.frame = CGRectOffset(_gridController.view.frame, 0, (self.startOnGrid ? -1 : 1) * self.view.bounds.size.height);
|
|
// Stop specific layout being triggered
|
_skipNextPagingScrollViewPositioning = YES;
|
|
// Add as a child view controller
|
[self addChildViewController:_gridController];
|
[self.view addSubview:_gridController.view];
|
|
// Perform any adjustments
|
[_gridController.view layoutIfNeeded];
|
[_gridController adjustOffsetsAsRequired];
|
|
// Hide action button on nav bar if it exists
|
if (self.navigationItem.rightBarButtonItem == _actionButton) {
|
_gridPreviousRightNavItem = _actionButton;
|
[self.navigationItem setRightBarButtonItem:nil animated:YES];
|
} else {
|
_gridPreviousRightNavItem = nil;
|
}
|
|
// Update
|
[self updateNavigation];
|
[self setControlsHidden:NO animated:YES permanent:YES];
|
|
// Animate grid in and photo scroller out
|
[_gridController willMoveToParentViewController:self];
|
[UIView animateWithDuration:animated ? 0.3 : 0 animations:^(void) {
|
_gridController.view.frame = self.view.bounds;
|
CGRect newPagingFrame = [self frameForPagingScrollView];
|
newPagingFrame = CGRectOffset(newPagingFrame, 0, (self.startOnGrid ? 1 : -1) * newPagingFrame.size.height);
|
_pagingScrollView.frame = newPagingFrame;
|
} completion:^(BOOL finished) {
|
[_gridController didMoveToParentViewController:self];
|
}];
|
|
}
|
|
- (void)hideGrid {
|
|
if (!_gridController) return;
|
|
// Remember previous content offset
|
_currentGridContentOffset = _gridController.collectionView.contentOffset;
|
|
// Restore action button if it was removed
|
if (_gridPreviousRightNavItem == _actionButton && _actionButton) {
|
[self.navigationItem setRightBarButtonItem:_gridPreviousRightNavItem animated:YES];
|
}
|
|
// Position prior to hide animation
|
CGRect newPagingFrame = [self frameForPagingScrollView];
|
newPagingFrame = CGRectOffset(newPagingFrame, 0, (self.startOnGrid ? 1 : -1) * newPagingFrame.size.height);
|
_pagingScrollView.frame = newPagingFrame;
|
|
// Remember and remove controller now so things can detect a nil grid controller
|
MWGridViewController *tmpGridController = _gridController;
|
_gridController = nil;
|
|
// Update
|
[self updateNavigation];
|
[self updateVisiblePageStates];
|
|
// Animate, hide grid and show paging scroll view
|
[UIView animateWithDuration:0.3 animations:^{
|
tmpGridController.view.frame = CGRectOffset(self.view.bounds, 0, (self.startOnGrid ? -1 : 1) * self.view.bounds.size.height);
|
_pagingScrollView.frame = [self frameForPagingScrollView];
|
} completion:^(BOOL finished) {
|
[tmpGridController willMoveToParentViewController:nil];
|
[tmpGridController.view removeFromSuperview];
|
[tmpGridController removeFromParentViewController];
|
[self setControlsHidden:NO animated:YES permanent:NO]; // retrigger timer
|
}];
|
|
}
|
|
#pragma mark - Control Hiding / Showing
|
|
// If permanent then we don't set timers to hide again
|
// Fades all controls on iOS 5 & 6, and iOS 7 controls slide and fade
|
- (void)setControlsHidden:(BOOL)hidden animated:(BOOL)animated permanent:(BOOL)permanent {
|
|
// Force visible
|
if (![self numberOfPhotos] || _gridController || _alwaysShowControls)
|
hidden = NO;
|
|
// Cancel any timers
|
[self cancelControlHiding];
|
|
// Animations & positions
|
CGFloat animatonOffset = 20;
|
CGFloat animationDuration = (animated ? 0.35 : 0);
|
|
// Status bar
|
if (!_leaveStatusBarAlone) {
|
|
// Hide status bar
|
if (!_isVCBasedStatusBarAppearance) {
|
|
// Non-view controller based
|
[[UIApplication sharedApplication] setStatusBarHidden:hidden withAnimation:animated ? UIStatusBarAnimationSlide : UIStatusBarAnimationNone];
|
|
} else {
|
|
// View controller based so animate away
|
_statusBarShouldBeHidden = hidden;
|
[UIView animateWithDuration:animationDuration animations:^(void) {
|
[self setNeedsStatusBarAppearanceUpdate];
|
} completion:^(BOOL finished) {}];
|
|
}
|
|
}
|
|
// Toolbar, nav bar and captions
|
// Pre-appear animation positions for sliding
|
if ([self areControlsHidden] && !hidden && animated) {
|
|
// Toolbar
|
_toolbar.frame = CGRectOffset([self frameForToolbarAtOrientation:self.interfaceOrientation], 0, animatonOffset);
|
|
// Captions
|
for (MWZoomingScrollView *page in _visiblePages) {
|
if (page.captionView) {
|
MWCaptionView *v = page.captionView;
|
// Pass any index, all we're interested in is the Y
|
CGRect captionFrame = [self frameForCaptionView:v atIndex:0];
|
captionFrame.origin.x = v.frame.origin.x; // Reset X
|
v.frame = CGRectOffset(captionFrame, 0, animatonOffset);
|
}
|
}
|
|
}
|
[UIView animateWithDuration:animationDuration animations:^(void) {
|
|
CGFloat alpha = hidden ? 0 : 1;
|
|
// Nav bar slides up on it's own on iOS 7+
|
[self.navigationController.navigationBar setAlpha:alpha];
|
|
// Toolbar
|
_toolbar.frame = [self frameForToolbarAtOrientation:self.interfaceOrientation];
|
if (hidden) _toolbar.frame = CGRectOffset(_toolbar.frame, 0, animatonOffset);
|
_toolbar.alpha = alpha;
|
|
// Captions
|
for (MWZoomingScrollView *page in _visiblePages) {
|
if (page.captionView) {
|
MWCaptionView *v = page.captionView;
|
// Pass any index, all we're interested in is the Y
|
CGRect captionFrame = [self frameForCaptionView:v atIndex:0];
|
captionFrame.origin.x = v.frame.origin.x; // Reset X
|
if (hidden) captionFrame = CGRectOffset(captionFrame, 0, animatonOffset);
|
v.frame = captionFrame;
|
v.alpha = alpha;
|
}
|
}
|
|
// Selected buttons
|
for (MWZoomingScrollView *page in _visiblePages) {
|
if (page.selectedButton) {
|
UIButton *v = page.selectedButton;
|
CGRect newFrame = [self frameForSelectedButton:v atIndex:0];
|
newFrame.origin.x = v.frame.origin.x;
|
v.frame = newFrame;
|
}
|
}
|
|
} completion:^(BOOL finished) {}];
|
|
// Control hiding timer
|
// Will cancel existing timer but only begin hiding if
|
// they are visible
|
if (!permanent) [self hideControlsAfterDelay];
|
|
}
|
|
- (BOOL)prefersStatusBarHidden {
|
if (!_leaveStatusBarAlone) {
|
return _statusBarShouldBeHidden;
|
} else {
|
return [self presentingViewControllerPrefersStatusBarHidden];
|
}
|
}
|
|
- (UIStatusBarStyle)preferredStatusBarStyle {
|
return UIStatusBarStyleLightContent;
|
}
|
|
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation {
|
return UIStatusBarAnimationSlide;
|
}
|
|
- (void)cancelControlHiding {
|
// If a timer exists then cancel and release
|
if (_controlVisibilityTimer) {
|
[_controlVisibilityTimer invalidate];
|
_controlVisibilityTimer = nil;
|
}
|
}
|
|
// Enable/disable control visiblity timer
|
- (void)hideControlsAfterDelay {
|
if (![self areControlsHidden]) {
|
[self cancelControlHiding];
|
_controlVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:self.delayToHideElements target:self selector:@selector(hideControls) userInfo:nil repeats:NO];
|
}
|
}
|
|
- (BOOL)areControlsHidden { return (_toolbar.alpha == 0); }
|
- (void)hideControls { [self setControlsHidden:YES animated:YES permanent:NO]; }
|
- (void)showControls { [self setControlsHidden:NO animated:YES permanent:NO]; }
|
- (void)toggleControls { [self setControlsHidden:![self areControlsHidden] animated:YES permanent:NO]; }
|
|
#pragma mark - Properties
|
|
- (void)setCurrentPhotoIndex:(NSUInteger)index {
|
// Validate
|
NSUInteger photoCount = [self numberOfPhotos];
|
if (photoCount == 0) {
|
index = 0;
|
} else {
|
if (index >= photoCount)
|
index = [self numberOfPhotos]-1;
|
}
|
_currentPageIndex = index;
|
if ([self isViewLoaded]) {
|
[self jumpToPageAtIndex:index animated:NO];
|
if (!_viewIsActive)
|
[self tilePages]; // Force tiling if view is not visible
|
}
|
}
|
|
#pragma mark - Misc
|
|
- (void)doneButtonPressed:(id)sender {
|
// Only if we're modal and there's a done button
|
if (_doneButton) {
|
// See if we actually just want to show/hide grid
|
if (self.enableGrid) {
|
if (self.startOnGrid && !_gridController) {
|
[self showGrid:YES];
|
return;
|
} else if (!self.startOnGrid && _gridController) {
|
[self hideGrid];
|
return;
|
}
|
}
|
// Dismiss view controller
|
if ([_delegate respondsToSelector:@selector(photoBrowserDidFinishModalPresentation:)]) {
|
// Call delegate method and let them dismiss us
|
[_delegate photoBrowserDidFinishModalPresentation:self];
|
} else {
|
[self dismissViewControllerAnimated:YES completion:nil];
|
}
|
}
|
}
|
|
#pragma mark - Actions
|
|
- (void)actionButtonPressed:(id)sender {
|
|
// Only react when image has loaded
|
id <MWPhoto> photo = [self photoAtIndex:_currentPageIndex];
|
if ([self numberOfPhotos] > 0 && [photo underlyingImage]) {
|
|
// If they have defined a delegate method then just message them
|
if ([self.delegate respondsToSelector:@selector(photoBrowser:actionButtonPressedForPhotoAtIndex:)]) {
|
|
// Let delegate handle things
|
[self.delegate photoBrowser:self actionButtonPressedForPhotoAtIndex:_currentPageIndex];
|
|
} else {
|
|
// Show activity view controller
|
NSMutableArray *items = [NSMutableArray arrayWithObject:[photo underlyingImage]];
|
if (photo.caption) {
|
[items addObject:photo.caption];
|
}
|
self.activityViewController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil];
|
|
// Show loading spinner after a couple of seconds
|
double delayInSeconds = 2.0;
|
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
|
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
|
if (self.activityViewController) {
|
[self showProgressHUDWithMessage:nil];
|
}
|
});
|
|
// Show
|
typeof(self) __weak weakSelf = self;
|
[self.activityViewController setCompletionHandler:^(NSString *activityType, BOOL completed) {
|
weakSelf.activityViewController = nil;
|
[weakSelf hideControlsAfterDelay];
|
[weakSelf hideProgressHUD:YES];
|
}];
|
// iOS 8 - Set the Anchor Point for the popover
|
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8")) {
|
self.activityViewController.popoverPresentationController.barButtonItem = _actionButton;
|
}
|
[self presentViewController:self.activityViewController animated:YES completion:nil];
|
|
}
|
|
// Keep controls hidden
|
[self setControlsHidden:NO animated:YES permanent:YES];
|
|
}
|
|
}
|
|
#pragma mark - Action Progress
|
|
- (MBProgressHUD *)progressHUD {
|
if (!_progressHUD) {
|
_progressHUD = [[MBProgressHUD alloc] initWithView:self.view];
|
_progressHUD.minSize = CGSizeMake(120, 120);
|
_progressHUD.minShowTime = 1;
|
[self.view addSubview:_progressHUD];
|
}
|
return _progressHUD;
|
}
|
|
- (void)showProgressHUDWithMessage:(NSString *)message {
|
self.progressHUD.labelText = message;
|
self.progressHUD.mode = MBProgressHUDModeIndeterminate;
|
[self.progressHUD show:YES];
|
self.navigationController.navigationBar.userInteractionEnabled = NO;
|
}
|
|
- (void)hideProgressHUD:(BOOL)animated {
|
[self.progressHUD hide:animated];
|
self.navigationController.navigationBar.userInteractionEnabled = YES;
|
}
|
|
- (void)showProgressHUDCompleteMessage:(NSString *)message {
|
if (message) {
|
if (self.progressHUD.isHidden) [self.progressHUD show:YES];
|
self.progressHUD.labelText = message;
|
self.progressHUD.mode = MBProgressHUDModeCustomView;
|
[self.progressHUD hide:YES afterDelay:1.5];
|
} else {
|
[self.progressHUD hide:YES];
|
}
|
self.navigationController.navigationBar.userInteractionEnabled = YES;
|
}
|
|
@end
|