移动开发iOS Developer

自定义导航控制器返回按钮和实现全屏滑动返回功能

2016-10-18  本文已影响618人  追风筝的boy

导航控制器的返回按钮设置以及一些细节处理

// 1.来源控制器的backBarButtonItem属性来设置
sourceVC.navigationItem.backBarButtonItem

// 2.目标口控制器的leftBarButtonItem属性来设置
destinationVC.navigatinoItem.leftBarButtonItem
    - (instancetype)init ;
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;
    - (instancetype)initWithImage:(nullable UIImage *)image style:(UIBarButtonItemStyle)style target:(nullable id)target action:(nullable SEL)action;
    - (instancetype)initWithImage:(nullable UIImage *)image landscapeImagePhone:(nullable UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style target:(nullable id)target action:(nullable SEL)action;
    - (instancetype)initWithTitle:(nullable NSString *)title style:(UIBarButtonItemStyle)style target:(nullable id)target action:(nullable SEL)action;
    - (instancetype)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(nullable id)target action:(nullable SEL)action;
    - (instancetype)initWithCustomView:(UIView *)customView;
    ```
- **需求:** 返回按钮在普通状态下和高亮状态下有自己的颜色和状态.
- **解决方法:** 一般会将`UIButton`对象包装成一个`UIBarButtonItem`对象来赋值给`leftBarButtonItem`.
- **问题:**如果直接将`UIButton`设置为`leftBarButtonItem`,系统会将按钮的点击返回扩大,影响用户体验.
- **方法改善:**
    - 1.自定义`UIView`,在里面封装一个`UIButton`,设置按钮的颜色和状态即可
        - **细节需求**: 需要将按钮向左边移动一段距离,这里我们假设为10(因为默认设置的返回按钮的位置和导航控制器最左边的间隙有点大)
        - 直接调整`UIView`的`frame`是不可以的,所以这里可以通过调整`UIButton`的`frame`或者`contentEdgeInsets`来达到显示的效果是向左边移动的.这里采用的是在`layoutSubviews`中直接修改`UIButton`的`frame`的`x`值的方法(具体代码如下)
        - **这里又出现一个小问题:**就是当我们设置按钮的x值为负值的时候(按钮相对于父控件是向左边移动的), 超出的父控件的部分是无法点击的.
            - **问题分析:**使用事件传递的知识,首先让我们来复习一下事件传递的过程
            
            ```objc
            1.首先当产生一个事件的时候,应用程序UIApplication会首先接收到这个事件,并将事件分发下去
            2.一般是将事件传递给主窗口,然后会调用view的hitTest:方法使用以下的三个步骤来找到窗口视图层次结构中最合适的view来处理事件
                2.1首先先判断当前的view是否可以接收事件(hidden == NO && userInteractionEnabled == YES && alpha > 0.01 满足这三个条件,表示可以接收事件)
                2.2满足上面条件以后,在判断当前的点是否在view上面
                2.3如果上面两个条件都满足,那么从后向前遍历view.subViews数组,在使用上面三个条件判断,直到找到最合适的view为止
            3.当找到最合适的view之后,会调用最合适的view的监听事件的方法,例如touchesBegan等方法(根据产生的事件不同调用不同的方法.),如果该view没有实现touchesBegan等方法,或者在这个方法中调用了[super touchesBegan], 那么事件会顺着响应者链条向上传递,调用上一个响应者的touchesBegan等方法,依次类推.
            ```
            - 从上面事件传递的过程中,我们可以得知,当点击返回按钮(超出父控件位置)的时候,这个点击事件会先传递给button的父控件, 调用父控件的hitTest:方法来判断button的父控件是否是合适的View.
            - 首先它会判断button的父控件是否可以处理事件(可以)
            - 然后判断点是不是在button的父控件上面,由于这个点的坐标是超出父控件的,所以在这里的判断就不成立,也就不会将事件传递给button了.
            - **解决方法:**当事件传递给Button的父控件的时候,我们在hitTest里面进行判断即可.具体解决代码如下(在`hitTest:`方法中)

      ```objc
      #import "JGBackView.h"
          
      @interface JGBackView ()
      /** 按钮 */
      @property (nonatomic, weak) UIButton *button;
      @end
          
      @implementation JGBackView
      - (instancetype)initWithFrame:(CGRect)frame
      {
          if (self = [super initWithFrame:frame]) {
            
            UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
            
            //imageWithOriginalMode:这个方法是我自己抽的分类,就是设置图片的渲染模式为originalMode
            [backButton setImage:[UIImage imageWithOriginalMode:@"navigationButtonReturn"] forState:UIControlStateNormal];
            [backButton setImage:[UIImage imageWithOriginalMode:@"navigationButtonReturnClick"] forState:UIControlStateHighlighted];
            [backButton setTitle:@"返回" forState:UIControlStateNormal];
            [backButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
            [backButton setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
            [backButton sizeToFit];
            self.button = backButton;
            [self addSubview:backButton];
             
          }
        return self;
      }
          
      // 将UIButton的事件监听方法传递出去
      - (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
      {
        [self.button addTarget:target action:action forControlEvents:controlEvents];
      }
          
      // 这里方法用来处理当按钮超出父控件的部分无法点击的问题
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      {
          // 将点的坐标(这个点的坐标坐标系是button的父控件,在这里就是self)转换成button坐标系上面的点的坐标
          CGPoint btnP = [self convertPoint:point toView:self.button];
          // 判断该点是不是在按钮上面
          if (CGRectContainsPoint(self.button.bounds, btnP)) {
             
          // 如果点在按钮上,那么这个按钮就是最合适的view(会让button来处理事件)
            return self.button;
         }
         
        // 恢复默认做法
        return [super hitTest:point withEvent:event];
      }
          
      - (void)layoutSubviews
      {
         [super layoutSubviews];
         
          // 设置按钮的x值为负值,即像左边移动
          self.button.jg_x -= 10;
             
          // 设置自身的尺寸
          self.jg_width = self.button.jg_width;
          self.jg_height = self.button.jg_height;
             
          // 如果这里设置了x值,那么在返回按钮刚显示的时候,会出现位置不正确的bug,所以这里我们不设置x,采用系统默认的x值就可以了
        self.jg_y = (44 - self.jg_height) * 0.5;
}
@end
    ```

- 全局设置导航栏上返回按钮(设置非根控制器的返回按钮)
    - 在导航控制跳转的时候,一定会调用控制器的`pushViewController:`这个方法,而在这方法中我们又可以拿到目的控制器,而我们推荐使用的就是使用目的控制器的`destinationVC.navigatinoItem.leftBarButtonItem`方法来设置返回按钮.
    - **解决方法:**自定义导航控制器,重写`pushViewController:`,在这个方法中来同意设置返回按钮(代码在`pushViewController:`方法中)

```objc
@interface JGNavigationController () <UIGestureRecognizerDelegate>
@end

@implementation JGNavigationController
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{

        if (self.childViewControllers.count != 0) { // 当前控制器为非根控制器的时候我们才需要设置返回按钮
            // 设置返回按钮
            JGBackView *backView = [[JGBackView alloc] init];
            [backView addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
            
            // 在这里统一设置返回按钮
            viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backView];
            viewController.hidesBottomBarWhenPushed = YES;
        }
        
        [super pushViewController:viewController animated:animated];
}

@end

自定义返回按钮的默认屏幕边缘滑动返回功能恢复

The gesture recognizer responsible for popping the top view controller off the navigation stack.
The navigation controller installs this gesture recognizer on its view and uses it to pop the topmost view controller off the navigation stack. You can use this property to retrieve the gesture recognizer and tie it to the behavior of other gesture recognizers in your user interface. When tying your gesture recognizers together, make sure they recognize their gestures simultaneously to ensure that your gesture recognizers are given a chance to handle the event.

这个手势是用来将栈顶控制器移除导航栈的.
导航控制器给它的view添加了这个手势, 用它来将栈顶的控制器移出导航栈.你可以使用这个属性来获取到这个手势对象,也可以将这个手势和你用户界面中的其他手势绑定在一起使用.当你将这些手势混合使用的时候,记得要调用手势的代理方法来允许可以识别多个手势来保证你的手势可以执行它指定的方法.(英语水平有限,只能翻译大概意思)
<UIScreenEdgePanGestureRecognizer: 0x7ff290717eb0; state = Possible; delaysTouchesBegan = YES; view = <UILayoutContainerView 0x7ff29051d3e0>; target= <(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7ff290712d00>)>>
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
// 这里我们根据当前导航控制器的子控制数量来判断当前的控制器是否为根控制器
    return self.childViewControllers.count > 1;
}

自定义返回按钮全屏滑动返回功能实现

- (void)viewDidLoad
{
    [super viewDidLoad];
    
        // 首先要静止系统自带的屏幕边缘滑动返回手势
    self.interactivePopGestureRecognizer.enabled = NO;
    // 1.自定义全屏滑动手势
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self.interactivePopGestureRecognizer.delegate action:@selector(handleNavigationTransition:)];
    // 2.设置代理,来判断当栈顶控制器跟控制器的时候,禁止识别手势
    pan.delegate = self;
    [self.view addGestureRecognizer:pan];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    // 判断当前的控制器是否为根控制器
    return self.childViewControllers.count > 1;
}

上一篇 下一篇

猜你喜欢

热点阅读