问题
Long story short, I have a view controller where the user can tap on self.view
(anywhere but the nav bar) and it will enter a full screen mode where the controls at the bottom fade out and the navigation and status bar fade out. Similar to iBooks.
I could simply fade the alpha of the navigation bar, but as to allow the user to tap in the newly gained area (where the navigation bar was now that it's faded out) and have it do something, I have to do more than change the alpha, as the nav bar is still technically there taking up area.
So I hide the navigation bar with [self.navigationController setNavigationBarHidden:YES animated:NO];
. I have to do this after the animation block finishes, else it will be in the animation block and animate as part of the block. So I use a dispatch_after
to make it finish after the animation completes (0.35 second delay).
However, this causes the issue where if the user taps any time during that 0.35 second time period where it's animating out and things are waiting to be finished, it causes glitchy behaviour where another block starts even though it's still waiting 0.35 seconds for the other one to finish. It causes some glitchy behaviour and causes the navigation bar to stay hidden. Gross.
Video of it happening: http://cl.ly/2i3H0k0Q1T0V
Here's my code to demonstrate what I'm doing:
- (void)hideControls:(BOOL)hidden {
self.navigationController.view.backgroundColor = self.view.backgroundColor;
int statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
[UIView animateWithDuration:0.35 animations:^{
[[UIApplication sharedApplication] setStatusBarHidden:hidden withAnimation:UIStatusBarAnimationFade];
if (hidden) {
self.navigationController.navigationBar.alpha = 0.0;
self.instructionsLabel.alpha = 0.0;
self.backFiftyWordsButton.alpha = 0.0;
self.forwardFiftyWordsButton.alpha = 0.0;
self.WPMLabel.alpha = 0.0;
self.timeRemainingLabel.alpha = 0.0;
}
else {
self.navigationController.navigationBar.alpha = 1.0;
self.instructionsLabel.alpha = 1.0;
self.backFiftyWordsButton.alpha = 1.0;
self.forwardFiftyWordsButton.alpha = 1.0;
self.WPMLabel.alpha = 1.0;
self.timeRemainingLabel.alpha = 1.0;
}
[self.view layoutIfNeeded];
}];
// Perform an "actual" hide (more than just alpha changes) after the animation finishes in order to regain that touch area
if (hidden) {
double delayInSeconds = 0.35;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
[self.navigationController setNavigationBarHidden:YES animated:NO];
self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE + self.navigationController.navigationBar.frame.size.height + statusBarHeight;
});
}
else {
[self.navigationController setNavigationBarHidden:NO animated:NO];
self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE;
}
}
The only other thing I'm doing is changing the constant on my Auto Layout constraint to account for the navigation bar and status bar dependent on whether or not they're there.
I'm not sure how to factor in the fact that double tapping can really glitch out the full screen process. How could I make it so if they tap during the animation process it will just cancel the animation and do their desired action as intended? Could I be doing this process better?
回答1:
I think you can do this without adjusting any frames or constraints using these principles:
1) Make the window's background color the same as your view's
2) Add a tap gesture recognizer to the window. This allows tapping anywhere on the screen (except the status bar when its alpha isn't 0) whether the navigation bar is visible or not. This allows you to not have to set the navigation bar to hidden which would cause your view to resize.
3) Use hitTest: in the tapper's action method to check if the user tapped the navigation bar, and don't fade out if the tap was there.
4) Use UIViewAnimationOptionBeginFromCurrentState and UIViewAnimationOptionAllowUserInteraction in the animation block so the fade-in or fade-out can be reversed smoothly with another touch.
5) Enclose all the bottom controls in a clear UIView so you can just fade out that UIView instead of all the individual controls.
Here is the code that worked:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.view.window.backgroundColor = self.view.backgroundColor;
UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fadeInFadeOut:)];
[self.view.window addGestureRecognizer:tapper];
}
-(void)fadeInFadeOut:(UITapGestureRecognizer *)sender {
static BOOL hide = YES;
id hitView = [self.navigationController.view hitTest:[sender locationInView:self.navigationController.view] withEvent:nil];
if (! [hitView isKindOfClass:[UINavigationBar class]] && hide == YES) {
hide = ! hide;
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade];
[UIView animateWithDuration:.35 delay:0 options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction animations:^{
self.navigationController.navigationBar.alpha = 0;
self.bottomView.alpha = 0;
} completion:nil];
}else if (hide == NO){
hide = ! hide;
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade];
[UIView animateWithDuration:.35 delay:0 options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction animations:^{
self.navigationController.navigationBar.alpha = 1;
self.bottomView.alpha = 1;
} completion:nil];
}
}
回答2:
The other answers are helpful, but one thing you should probably do is instead of hard-coding your animation duration to 0.35, try using UINavigationControllerHideShowBarDuration
. This will make your app more resilient to changes to UIKit behavior.
回答3:
Why not use the animationCompleted delegate or block?
回答4:
The way I'd do this is simply create a BOOL
flag that I'd call something like isTransitioning
, such that once the hiding/unhiding process starts, the hideControls
method returns immediately if a transition is in progress. That way you're not messing with touch events; you're directly stopping the unwanted glitches without causing side-effects elsewhere (you'll need to declare isTransitioning
as a property/ivar outside of the method, obviously):
- (void)hideControls:(BOOL)hidden {
//Check there isn't a hide/unhide already in progress:
if(self.isTransitioning == YES) return;
//if there wasn't already a transition in progress, set
//isTransitioning to YES and off we go:
self.isTransitioning = YES;
self.navigationController.view.backgroundColor = self.view.backgroundColor;
int statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
[UIView animateWithDuration:0.35 animations:^{
[[UIApplication sharedApplication] setStatusBarHidden:hidden withAnimation:UIStatusBarAnimationFade];
if (hidden) {
self.navigationController.navigationBar.alpha = 0.0;
self.instructionsLabel.alpha = 0.0;
self.backFiftyWordsButton.alpha = 0.0;
self.forwardFiftyWordsButton.alpha = 0.0;
self.WPMLabel.alpha = 0.0;
self.timeRemainingLabel.alpha = 0.0;
}
else {
self.navigationController.navigationBar.alpha = 1.0;
self.instructionsLabel.alpha = 1.0;
self.backFiftyWordsButton.alpha = 1.0;
self.forwardFiftyWordsButton.alpha = 1.0;
self.WPMLabel.alpha = 1.0;
self.timeRemainingLabel.alpha = 1.0;
}
[self.view layoutIfNeeded];
}];
// Perform an "actual" hide (more than just alpha changes) after the animation finishes in order to regain that touch area
if (hidden) {
double delayInSeconds = 0.35;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
[self.navigationController setNavigationBarHidden:YES animated:NO];
self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE + self.navigationController.navigationBar.frame.size.height + statusBarHeight;
//Unset isTransitioning now we're finished:
self.isTransitioning = NO;
});
}
else {
[self.navigationController setNavigationBarHidden:NO animated:NO];
self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE;
//Unset isTransitioning now we're finished:
self.isTransitioning = NO;
}
}
I haven't directly tested this code, but you can see what I'm getting at, I'm sure.
回答5:
EDIT #2
looking at the docs, hitTest:withEvent:
simply calls pointTest:withEvent:
on all subviews of the view. It clearly states that views with alpha level less than 0.01 are ignored. I think we are on the right path here, just need to explore further. I'm sure there's a way to have a view with alpha == 0.0f
pass through touches to any views beneath it. Hopefully you (or someone else here) will get it. If I have time I'll dive into some code and try to help further.
EDIT #1: try overriding pointInside:withEvent:
Sorry for the stream of consciousness answer here. Normally I would either test this myself or paste code from a production app but I'm too busy right now.
I think overriding pointInside:withEvent
will work:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (self.alpha == 0.0f) {
return NO;
}
else {
return [super pointInside:point withEvent:event];
}
}
ORIGINAL ANSWER:
I would try subclassing UINavigationBar
and overriding hitTest:withEvent:
so that the navigation bar ignores touches while it is invisible. I didn't test this but should be something like:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// if alpha == 0.0f, the nav bar should ignore touches
if (self.alpha == 0.0f) {
return nil;
}
// else hitTest as normal
else {
return [super hitTest:point withEvent:event];
}
}
If testing for alpha == 0.0f isn't how you want to decide to ignore touches, you could also add your own property and do it that way.
@property (nonatomic) BOOL ignoreTouches;
In your hitTest:withEvent:
implementation check for if (ignoreTouches)
and return nil.
回答6:
To turn off receiving touch events while you animate the UINavigationBar
then subclass it and add the following method:
-(BOOL) canBecomeFirstResponder{
NSLog(@"Decide if to allow the tap through");
if (self.hiding) return NO;
return YES;
}
Then you can control if and when you allow touches to be responded to.
Keep in mind that UINavigationBar
is a subclass of UIResponder
which allows you to override this and many other methods. I also often forget to look up the inherit chain.
回答7:
You could use animateWithDuration:animations:completion:
. The completion block is executed after the animations have completed (source). It also has an added benefit; if you decide to change the timing of the animations in the future, you won't have to worry changing the timing in two places.
[UIView animateWithDuration:0.35 animations:^{
[[UIApplication sharedApplication] setStatusBarHidden:hidden withAnimation:UIStatusBarAnimationFade];
if (hidden) {
self.navigationController.navigationBar.alpha = 0.0;
self.instructionsLabel.alpha = 0.0;
self.backFiftyWordsButton.alpha = 0.0;
self.forwardFiftyWordsButton.alpha = 0.0;
self.WPMLabel.alpha = 0.0;
self.timeRemainingLabel.alpha = 0.0;
}
else {
self.navigationController.navigationBar.alpha = 1.0;
self.instructionsLabel.alpha = 1.0;
self.backFiftyWordsButton.alpha = 1.0;
self.forwardFiftyWordsButton.alpha = 1.0;
self.WPMLabel.alpha = 1.0;
self.timeRemainingLabel.alpha = 1.0;
}
[self.view layoutIfNeeded];
}
completion:^(BOOL finished){
// Perform an "actual" hide (more than just alpha changes) after the animation finishes in order to regain that touch area
if ( finished ) {
if (hidden) {
[self.navigationController setNavigationBarHidden:YES animated:NO];
self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE + self.navigationController.navigationBar.frame.size.height + statusBarHeight;
}
else {
[self.navigationController setNavigationBarHidden:NO animated:NO];
self.textToReadLabelPositionFromTopConstraint.constant = TEXT_LABEL_DISTANCE;
}
}
}];
Based on the comments, you could disable the navigation bar before the animations start, and re-enable in the completion block, but that is up to you test what works best for you..
回答8:
Rather than messing with views in the nav-controller stack, why not have a FullScreenViewController (from UIViewController) and make that the root of your app. Then add the NavController (and its stack) on top of that. When the time comes, just fade the whole NavController, exposing your FullScreenViewController.
(You could do this idea upside down, too -- something like this (typed in browser -- lots of syntax errors!):
UIViewController *vc = // ... FullScreenViewController
vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[navController presentViewController: vc animated: YES completion: nil];
NOTE: you can also make use of childViewControllers to have an abstract class that contains the VC which will be both the full-screen and the not-full screen versions, and then just steal its view, as desired.
来源:https://stackoverflow.com/questions/18021634/how-can-i-make-it-so-if-i-fade-my-navigation-bar-out-then-actually-hide-it-prog