2015-10-22 19:54:15 szyangzhen 阅读数 3882

这个是参照鸿洋的android侧滑菜单实现的,同样利用scrollview实现,很简单,这是鸿洋侧滑的实现链接打造最简单的自定义侧滑菜单

第一种测滑

#import <UIKit/UIKit.h>

@interface SlideMenu : UIScrollView
@property (nonatomic, assign) BOOL isMenuOpen;
/**设置菜单和内容*/
-(void)setMenuView:(UIView*)menuView withContentView:(UIView*)contentView;
/**关闭菜单*/
-(void)closeMenuWithAnimation:(BOOL)animated;
/**打开菜单*/
-(void)openMenuWithAnimaton:(BOOL)animated;
/**菜单打开与关闭切换*/
-(void)toggleMenu;
@end
#import "SlideMenu.h"
#define menu_ratio 0.7f

@interface SlideMenu()<UIScrollViewDelegate>
@end

@implementation SlideMenu
{
    float _menuWidth;
    UIView *_menuViewContainer;
    UIView *_contentViewContainer;
    UIView *_menuView;
    UIView *_contentView;
}

-(instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if(self)
    {
        [self setUp];
    }
    return self;
}

-(void)setUp
{
    _menuViewContainer = [UIView new];
    _contentViewContainer = [UIView new];
    [self addSubview:_menuViewContainer];
    [self addSubview:_contentViewContainer];
    self.delegate = self;
    //不显示滚动条
    self.showsHorizontalScrollIndicator = false;
    //不显示弹簧效果
    self.bounces = false;
    _isMenuOpen = true;

    if(!CGRectEqualToRect(self.frame, CGRectZero))
    {
        [self initFrame];
    }
}

-(void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    [self initFrame];
}

-(void)initFrame
{
    //放置菜单的位置
    _menuViewContainer.frame = CGRectMake(0, 0, self.frame.size.width * menu_ratio, self.frame.size.height);
    _menuWidth = _menuViewContainer.frame.size.width;

    //放置content的位置
    _contentViewContainer.frame = CGRectMake(CGRectGetMaxX(_menuViewContainer.frame), 0, self.frame.size.width, self.frame.size.height);
    //设置可滚动区域
    self.contentSize = CGSizeMake(_menuViewContainer.frame.size.width + _contentViewContainer.frame.size.width, 0);
    //初始关闭菜单
    [self closeMenuWithAnimation:false];

    if(_menuView != nil)
    {
        _menuView.frame = _menuViewContainer.bounds;
    }
    if(_contentView != nil)
    {
        _contentView.frame = _contentViewContainer.bounds;
    }
}

//设置菜单和内容
-(void)setMenuView:(UIView *)menuView withContentView:(UIView *)contentView
{
    //添加菜单
    if(_menuView != nil)
    {
        return;
    }
    _menuView = menuView;
    [_menuViewContainer addSubview:menuView];

    //添加内容
    if(_contentView != nil)
    {
        return;
    }
    _contentView = contentView;
    [_contentViewContainer addSubview:contentView];

    //设置实际菜单位置,填充菜单容器
    _menuView.frame = _menuViewContainer.bounds;
    //设置实际内容位置,填充内容容器
    _contentView.frame = _contentViewContainer.bounds;

}


//拖动松开时会调用,第二个参数表示拖动松开后是否会自动减速滑动
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    //判断应该显示菜单还是内容
    float offset = scrollView.contentOffset.x;
    if(decelerate)
    {
        //如果松开后自动减速滚动,什么也不做
        return;
    }
    if(offset > _menuWidth / 2.0f)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    //判断应该显示菜单还是内容
    float offset = scrollView.contentOffset.x;

    if(offset > _menuWidth / 2.0f)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }

}


//关闭菜单
-(void)closeMenuWithAnimation:(BOOL)animated
{
    _isMenuOpen = false;
    [self setContentOffset:CGPointMake(_menuWidth, 0) animated:animated];
}
//打开菜单
-(void)openMenuWithAnimaton:(BOOL)animated
{
    _isMenuOpen = true;
    [self setContentOffset:CGPointMake(0, 0) animated:animated];
}

//菜单切换
-(void)toggleMenu
{
    if(_isMenuOpen)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }
}

第二种侧滑

#import <UIKit/UIKit.h>

@interface SlideMenu2 : UIScrollView
@property (nonatomic, assign) BOOL isMenuOpen;
/**设置菜单和内容*/
-(void)setMenuView:(UIView*)menuView withContentView:(UIView*)contentView;
/**关闭菜单*/
-(void)closeMenuWithAnimation:(BOOL)animated;
/**打开菜单*/
-(void)openMenuWithAnimaton:(BOOL)animated;
/**菜单打开与关闭切换*/
-(void)toggleMenu;
@end
#import "SlideMenu2.h"
#define menu_ratio 0.70f

@interface SlideMenu2()<UIScrollViewDelegate>
@end

@implementation SlideMenu2
{
    float _menuWidth;
    UIView *_menuViewContainer;
    UIView *_contentViewContainer;
    UIView *_menuView;
    UIView *_contentView;
}

-(instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if(self)
    {
        [self setUp];
    }
    return self;
}

-(void)setUp
{
    _menuViewContainer = [UIView new];
    _contentViewContainer = [UIView new];
    [self addSubview:_menuViewContainer];
    [self addSubview:_contentViewContainer];
    self.delegate = self;
    //不显示滚动条
    self.showsHorizontalScrollIndicator = false;
    //不显示弹簧效果
    self.bounces = false;
    _isMenuOpen = true;

    if(!CGRectEqualToRect(self.frame, CGRectZero))
    {
        [self initFrame];
    }
}

-(void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    [self initFrame];
}


-(void)initFrame
{
    //放置菜单的位置
    _menuViewContainer.frame = CGRectMake(0, 0, self.frame.size.width * menu_ratio, self.frame.size.height);
    _menuWidth = _menuViewContainer.frame.size.width;

    //放置content的位置
    _contentViewContainer.frame = CGRectMake(CGRectGetMaxX(_menuViewContainer.frame), 0, self.frame.size.width, self.frame.size.height);
    //设置可滚动区域
    self.contentSize = CGSizeMake(_menuViewContainer.frame.size.width + _contentViewContainer.frame.size.width, 0);
    //初始关闭菜单
    [self closeMenuWithAnimation:false];

    if(_menuView != nil)
    {
        _menuView.frame = _menuViewContainer.bounds;
    }
    if(_contentView != nil)
    {
        _contentView.frame = _contentViewContainer.bounds;
    }
}

//设置菜单和内容
-(void)setMenuView:(UIView *)menuView withContentView:(UIView *)contentView
{
    //添加菜单
    if(_menuView != nil)
    {
        return;
    }
    _menuView = menuView;
    [_menuViewContainer addSubview:menuView];

    //添加内容
    if(_contentView != nil)
    {
        return;
    }
    _contentView = contentView;
    [_contentViewContainer addSubview:contentView];

    //设置实际菜单位置,填充菜单容器
    _menuView.frame = _menuViewContainer.bounds;
    //设置实际内容位置,填充内容容器
    _contentView.frame = _contentViewContainer.bounds;

}


//拖动松开时会调用,第二个参数表示拖动松开后是否会自动减速滑动
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    //判断应该显示菜单还是内容
    float offset = scrollView.contentOffset.x;
    if(decelerate)
    {
        //如果松开后自动减速滚动,什么也不做
        return;
    }
    if(offset > _menuWidth / 2.0f)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }
}
//滚动停止后调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    //判断应该显示菜单还是内容
    float offset = scrollView.contentOffset.x;

    if(offset > _menuWidth / 2.0f)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }

}

//正在滚动时调用,频繁调用
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    //计算菜单移动比例
    float ratio = scrollView.contentOffset.x * 1.0f / _menuWidth;
    _menuViewContainer.transform = CGAffineTransformMakeTranslation(_menuWidth * ratio, 0);
}


//关闭菜单
-(void)closeMenuWithAnimation:(BOOL)animated
{
    _isMenuOpen = false;
    [self setContentOffset:CGPointMake(_menuWidth, 0) animated:animated];
}
//打开菜单
-(void)openMenuWithAnimaton:(BOOL)animated
{
    _isMenuOpen = true;
    [self setContentOffset:CGPointMake(0, 0) animated:animated];
}

//菜单切换
-(void)toggleMenu
{
    if(_isMenuOpen)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }
}


@end

仿QQ侧滑

#import <UIKit/UIKit.h>

@interface SlideMenu3 : UIScrollView
@property (nonatomic, assign) BOOL isMenuOpen;
/**设置菜单和内容*/
-(void)setMenuView:(UIView*)menuView withContentView:(UIView*)contentView;
/**关闭菜单*/
-(void)closeMenuWithAnimation:(BOOL)animated;
/**打开菜单*/
-(void)openMenuWithAnimaton:(BOOL)animated;
/**菜单打开与关闭切换*/
-(void)toggleMenu;
@end
#import "SlideMenu3.h"
#define menu_ratio 0.70f

@interface SlideMenu3()<UIScrollViewDelegate>
@end

@implementation SlideMenu3
{
    float _menuWidth;
    UIView *_menuViewContainer;
    UIView *_contentViewContainer;
    UIView *_menuView;
    UIView *_contentView;
}

-(instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if(self)
    {
        [self setUp];
    }
    return self;
}

-(void)setUp
{
    _menuViewContainer = [UIView new];
    _contentViewContainer = [UIView new];
    [self addSubview:_menuViewContainer];
    [self addSubview:_contentViewContainer];
    self.delegate = self;
    //这两行代码主要是为了确定缩放点,如果不设置是绕view的中心缩放
//    _contentViewContainer.layer.anchorPoint = CGPointMake(0, 0.5f);
//    _contentViewContainer.layer.position = CGPointMake(0, 0.5f);

    //不显示滚动条
    self.showsHorizontalScrollIndicator = false;
    //不显示弹簧效果
    self.bounces = false;
    _isMenuOpen = true;

    if(!CGRectEqualToRect(self.frame, CGRectZero))
    {
        [self initFrame];
    }
}

-(void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    [self initFrame];
}

-(void)initFrame
{
    //放置菜单的位置
    _menuViewContainer.frame = CGRectMake(0, 0, self.frame.size.width * menu_ratio, self.frame.size.height);
    _menuWidth = _menuViewContainer.frame.size.width;

    //放置content的位置
    _contentViewContainer.frame = CGRectMake(CGRectGetMaxX(_menuViewContainer.frame), 0, self.frame.size.width, self.frame.size.height);
    //设置可滚动区域
    self.contentSize = CGSizeMake(_menuViewContainer.frame.size.width + _contentViewContainer.frame.size.width, 0);
    //初始关闭菜单
    [self closeMenuWithAnimation:false];

    if(_menuView != nil)
    {
        _menuView.frame = _menuViewContainer.bounds;
    }
    if(_contentView != nil)
    {
        _contentView.frame = _contentViewContainer.bounds;
    }
}

//设置菜单和内容
-(void)setMenuView:(UIView *)menuView withContentView:(UIView *)contentView
{
    //添加菜单
    if(_menuView != nil)
    {
        return;
    }
    _menuView = menuView;
    [_menuViewContainer addSubview:menuView];

    //添加内容
    if(_contentView != nil)
    {
        return;
    }
    _contentView = contentView;
    [_contentViewContainer addSubview:contentView];

    //设置实际菜单位置,填充菜单容器
    _menuView.frame = _menuViewContainer.bounds;
    //设置实际内容位置,填充内容容器
    _contentView.frame = _contentViewContainer.bounds;
}


//拖动松开时会调用,第二个参数表示拖动松开后是否会自动减速滑动
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    //判断应该显示菜单还是内容
    float offset = scrollView.contentOffset.x;
    if(decelerate)
    {
        //如果松开后自动减速滚动,什么也不做
        return;
    }
    if(offset > _menuWidth / 2.0f)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }
}
//正在滚动时调用,频繁调用
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    //计算菜单移动比例
    float ratio = scrollView.contentOffset.x * 1.0f / _menuWidth;
    //计算缩放比例
    float contentScale = ratio * 0.2f + 0.8f;
    float menuScale = 1.0f - ratio * 0.3f;
    float alpha = 1.0f - ratio * 0.4f;

    //缩放内容区
    _contentViewContainer.transform = CGAffineTransformMakeScale(contentScale, contentScale);
    //处理菜单
    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(menuScale, menuScale);
     _menuViewContainer.transform = CGAffineTransformTranslate(scaleTransform,_menuWidth * ratio * 0.6, 0.0f);
    //菜单alpha
    _menuViewContainer.alpha = alpha;

}

//滚动停止后调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    //判断应该显示菜单还是内容
    float offset = scrollView.contentOffset.x;

    if(offset > _menuWidth / 2.0f)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }

}


//关闭菜单
-(void)closeMenuWithAnimation:(BOOL)animated
{
    _isMenuOpen = false;
    [self setContentOffset:CGPointMake(_menuWidth, 0) animated:animated];
}

//打开菜单
-(void)openMenuWithAnimaton:(BOOL)animated
{
    _isMenuOpen = true;
    [self setContentOffset:CGPointMake(0, 0) animated:animated];
}

//菜单切换
-(void)toggleMenu
{
    if(_isMenuOpen)
    {
        [self closeMenuWithAnimation:true];
    }
    else
    {
        [self openMenuWithAnimaton:true];
    }
}
@end

Demo下载地址

2016-05-25 21:24:26 zhaojinqiang12 阅读数 3687

方案一 :

开启使用系统自带的侧滑返回

iOS7之后系统提供了侧滑手势(interactivePopGestureRecognizer),即从屏幕左侧边缘滑起会pop回导航控制器栈的上个viewController。不过如果你自定义了UINavigationViewController或者自定义了返回按钮,系统自带的侧滑返回功能会失效。此时需要添加下面的代码解决:

self.navigationController.interactivePopGestureRecognizer.delegate = (id)self;

缺点:

  • 必须从屏幕边缘左侧滑起才会触发;
  • 一旦自定义导航控制器或者自定义返回按钮,就会失效。

方案二

实现UINavigationViewController的代理方法,自定义动画对象和交互对象。(即自定义转场动画)

这是苹果官方在WWDC上提倡的方法,灵活性高。可以高度自定义push和pop转场动画。
这种方法需要我们彻底实现侧滑返回,那我们的思路就是:

  • 先给view添加一个UIPanGestureRecognizer手势;
  • 再自定义该手势的触发方法,该方法里实现了侧滑。
1. 先创建一个BaseViewController,给该控制器的view添加拖动手势;
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.navigationController.delegate = self; // 设置navigationController的代理为self,并实现其代理方法

    self.view.userInteractionEnabled = YES;
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(backHandle:)];
    [self.view addGestureRecognizer:panGesture];
}


- (void)backHandle:(UIPanGestureRecognizer *)recognizer
{
    [self customControllerPopHandle:recognizer];
}
2.侧滑手势会触发这个回调方法;
- (void)customControllerPopHandle:(UIPanGestureRecognizer *)recognizer
{
    if(self.navigationController.childViewControllers.count == 1)
    {
        return;
    }
    // _interactiveTransition就是代理方法2返回的交互对象,我们需要更新它的进度来控制POP动画的流程。(以手指在视图中的位置与屏幕宽度的比例作为进度)
    CGFloat process = [recognizer translationInView:self.view].x/self.view.bounds.size.width;
    process = MIN(1.0, MAX(0.0, process));

    if(recognizer.state == UIGestureRecognizerStateBegan)
    {
        // 此时,创建一个UIPercentDrivenInteractiveTransition交互对象,来控制整个过程中动画的状态
        _interactiveTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
        [self.navigationController popViewControllerAnimated:YES];
    }
    else if(recognizer.state == UIGestureRecognizerStateChanged)
    {
        [_interactiveTransition updateInteractiveTransition:process]; // 更新手势完成度
    }
    else if(recognizer.state == UIGestureRecognizerStateEnded ||recognizer.state == UIGestureRecognizerStateCancelled)
    {
        // 手势结束时,若进度大于0.5就完成pop动画,否则取消
        if(process > 0.5)
        {
            [_interactiveTransition finishInteractiveTransition];
        }
        else
        {
            [_interactiveTransition cancelInteractiveTransition];
        }

        _interactiveTransition = nil;
    }
}
3.实现UINavigationControllerDelegate的两个协议方法,分别返回自定义动画需要的两个重要对象;
// 代理方法1:
// 返回一个实现了UIViewControllerAnimatedTransitioning协议的对象    ,即完成转场动画的对象
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
    if(operation == UINavigationControllerOperationPop) // 若operation是pop,就返回我们自定义的转场动画对象
    {
        return [[POPAnimation alloc] init];
    }

    return nil;
}


// 代理方法2
// 返回一个实现了UIViewControllerInteractiveTransitioning协议的对象,即完成动画交互(动画进度)的对象
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
    if([animationController isKindOfClass:[POPAnimation class]])
    {
        return _interactiveTransition;
    }
    return nil;
}
4.创建一个自定义动画类:POPAnimation。这是自定义动画的核心
  • 自定义动画类,即一个实现了UIViewControllerAnimatedTransitioning协议的类。
  • 实现该协议的两个方法,一个返回动画执行时间,一个自定义动画。
#import "POPAnimation.h"


@interface POPAnimation ()

@end

@implementation POPAnimation

// 实现两个协议的方法

// 返回动画执行的时间
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return 0.25;
}


//
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    __block UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; // 动画来自哪个vc
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; // 转场到哪个vc

    // 转场动画是两个控制器视图的动画,需要一个containerView作为“舞台”
    UIView *containerView = [transitionContext containerView];
    [containerView insertSubview:toVC.view belowSubview:fromVC.view];

    NSTimeInterval duration = [self transitionDuration:transitionContext]; // 获取动画执行时间(实现的协议方法)

    // 执行动画,让fromVC的view移动到屏幕最右侧
    [UIView animateWithDuration:duration animations:^{
        fromVC.view.transform = CGAffineTransformMakeTranslation([UIScreen mainScreen].bounds.size.width, 0);
    } completion:^(BOOL finished) {
        // 当动画执行完时,这个方法必须要调用,否则系统会认为你的其余操作都在动画执行过程中
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];

}


@end

pop.gif

方案三

极其简单取巧的方法

iOS7之后是有侧滑返回手势功能的。注意,也就是说系统已经定义了一种手势,并且给这个手势已经添加了一个触发方法(重点)。但是,系统的这个手势的触发条件是必须从屏幕左边缘开始滑动。我们取巧的方法是自己写一个支持全屏滑动的手势,而其触发方法系统已经有,没必要自己实现pop的动画,所以直接就把系统的触发处理方法作为我们自己定义的手势的处理方法。

#import "ViewController.h"

@interface ViewController ()<UIGestureRecognizerDelegate>

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    id target = self.navigationController.interactivePopGestureRecognizer.delegate;

    // handleNavigationTransition:为系统私有API,即系统自带侧滑手势的回调方法,我们在自己的手势上直接用它的回调方法
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:target action:@selector(handleNavigationTransition:)];
    panGesture.delegate = self; // 设置手势代理,拦截手势触发
    [self.view addGestureRecognizer:panGesture];

    // 一定要禁止系统自带的滑动手势
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}


// 什么时候调用,每次触发手势之前都会询问下代理方法,是否触发
// 作用:拦截手势触发
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    // 当当前控制器是根控制器时,不可以侧滑返回,所以不能使其触发手势
    if(self.navigationController.childViewControllers.count == 1)
    {
        return NO;
    }

    return YES;
}


@end

拓展

方案二涉及到了自定义转场动画的东西。关于自定义转场动画有三个核心的协议:

  • UIViewControllerAnimatedTransitioning:遵从该协议的对象,即是我们自定义的动画;
  • UIViewControllerInteractiveTransitioning:遵从该协议实现动画可交互性。不过一般我们直接使用系统UIPercentDrivenInteractiveTransition类,不需自定义。
  • UIViewControllerContextTransitioning:遵从该协议,定义了转场时需要的元数据。一般不需自己定义。



2018-10-22 17:50:57 lovingning 阅读数 123

在IOS中,在过度滑动时,整体布局会进行偏移,松手后,会出现回弹效果

安卓中则大多数控件都没有这种功能,在这里,可以自定义一个ViewGroup容器,针对该容器包裹的内容,可以进行过度滚动

在这里插入图片描述

为了实现,我 们需要进行接下来的处理

一、 创建ViewGroup布局

因为是要实现一个容器,因此需要自定义一个ViewGroup,然后重载构造函数,这里定义该控件名字为:OverScrollContainer

/**
 * Created on 2018/10/10  15:31
 * function : 包裹容器;用于在view外部形成一个可以over-scroll的视图
 *
 *
 * 只能有一个子view,否则,视图将加载错乱
 * 仅支持纵轴过度滑动
 *
 * @author mnlin
 */
class OverScrollContainer : ViewGroup {
    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}

如代码注释描述,该控件只支持纵轴方向的滚动,只能有一个Child

二、 定义可使用的属性

在初始阶段,我们定义的属性会简单一些,只要可以满足基本的需要就好。

纵轴滚动有两种方式:向上、向下

那么对应的,我们需要暴露以下的基本属性:

<!--弹性 布局 距离-->
<declare-styleable name="OverScrollContainer">
    <!--弹性滚动顶部的距离-->
    <attr name="overScrollDimen_top" format="dimension"/>
    <!--弹性滚动底部的距离-->
    <attr name="overScrollDimen_bottom" format="dimension"/>
    <!--超出滚动部分的颜色-->
    <attr name="overScroll_bg_color" format="color"/>
    <!--顶部过度滚动效果-->
    <attr name="enable_top" format="boolean"/>
    <!--底部过度滚动效果-->
    <attr name="enable_bottom" format="boolean"/>
</declare-styleable>

这样,在使用时,就可以根据说明,轻易的配置控件显示效果

接下来我们需要考虑,怎么来使用这些属性;

毋庸置疑的是,这些属性都将作用于OverScrollContainer控件,如果我们定义的是View控件,而非ViewGroup,那就没的商量,只要把这些 属性在xml布局中,<OverScrollContainer>标签上配置一下就好。但对于一个ViewGroup来说,为了使用这些属性,一般都会定义自己的LayoutParams,通过LayoutParams来处理效果的显示

就像FrameLayout一样:

/**
 * Per-child layout information for layouts that support margins.
 * See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes}
 * for a list of all child view attributes that this class supports.
 *
 * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
 */
public static class LayoutParams extends MarginLayoutParams {
    /**
     * Value for {@link #gravity} indicating that a gravity has not been
     * explicitly specified.
     */
    public static final int UNSPECIFIED_GRAVITY = -1;

    /**
     * The gravity to apply with the View to which these layout parameters
     * are associated.
     * <p>
     * The default value is {@link #UNSPECIFIED_GRAVITY}, which is treated
     * by FrameLayout as {@code Gravity.TOP | Gravity.START}.
     *
     * @see android.view.Gravity
     * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
     */
    public int gravity = UNSPECIFIED_GRAVITY;

    public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
        super(c, attrs);

        final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
        gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
        a.recycle();
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    /**
     * Creates a new set of layout parameters with the specified width, height
     * and weight.
     *
     * @param width the width, either {@link #MATCH_PARENT},
     *              {@link #WRAP_CONTENT} or a fixed size in pixels
     * @param height the height, either {@link #MATCH_PARENT},
     *               {@link #WRAP_CONTENT} or a fixed size in pixels
     * @param gravity the gravity
     *
     * @see android.view.Gravity
     */
    public LayoutParams(int width, int height, int gravity) {
        super(width, height);
        this.gravity = gravity;
    }

    public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
        super(source);
    }

    public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) {
        super(source);
    }

    /**
     * Copy constructor. Clones the width, height, margin values, and
     * gravity of the source.
     *
     * @param source The layout params to copy from.
     */
    public LayoutParams(@NonNull LayoutParams source) {
        super(source);

        this.gravity = source.gravity;
    }
}

这样一来,在FrameLayout布局中,子View只需要指定gravity,就可以调整自己在父布局中的显示效果。

更重要的是,使用LayoutParams的话,将来想要添加一些新的属性时,会方便的多。

我们仿照FrameLayout,来实现OverScrollContainer对应的LayoutParams

/**
 * 自定义layoutParams,便于实现自定义的属性
 */
class LayoutParams : ViewGroup.MarginLayoutParams {
    /**
     * over-scroll的距离
     */
    var overScrollDimenTop: Int = 96
    var overScrollDimenBottom: Int = 96

    /**
     * 是否可以回弹
     */
    var topEnable = true
    var bottomEnable = true

    /**
     * 滚动后留存的背景颜色
     */
    var bgColor = 0
    var bgDrawable: Drawable = ColorDrawable(Color.TRANSPARENT)

    constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
        val a = c.obtainStyledAttributes(attrs, R.styleable.OverScrollContainer)
        overScrollDimenTop = a.getDimensionPixelSize(R.styleable.OverScrollContainer_overScrollDimen_top, 96)
        overScrollDimenBottom = a.getDimensionPixelSize(R.styleable.OverScrollContainer_overScrollDimen_bottom, 96)
        topEnable = a.getBoolean(R.styleable.OverScrollContainer_enable_top, true)
        bottomEnable = a.getBoolean(R.styleable.OverScrollContainer_enable_bottom, true)
        bgColor = a.getColor(R.styleable.OverScrollContainer_overScroll_bg_color, 0)
        bgDrawable = ColorDrawable(bgColor)
        a.recycle()
    }

    constructor(width: Int, height: Int) : super(width, height)

    constructor(source: ViewGroup.LayoutParams) : super(source)

    constructor(source: ViewGroup.MarginLayoutParams) : super(source)

    constructor(source: OverScrollContainer.LayoutParams) : super(source) {
        overScrollDimenTop = source.overScrollDimenTop
        overScrollDimenBottom = source.overScrollDimenBottom
        topEnable = source.topEnable
        bottomEnable = source.bottomEnable
        bgColor = source.bgColor
        bgDrawable = source.bgDrawable
    }
}

然后在自定义的 ViewGroup 中,重写 LayoutParams的生成方法:

override fun generateDefaultLayoutParams(): OverScrollContainer.LayoutParams {
    return OverScrollContainer.LayoutParams(MATCH_PARENT, MATCH_PARENT)
}

override fun generateLayoutParams(attrs: AttributeSet): OverScrollContainer.LayoutParams {
    return OverScrollContainer.LayoutParams(context, attrs)
}

override fun generateLayoutParams(lp: ViewGroup.LayoutParams): ViewGroup.LayoutParams {
    if (lp is OverScrollContainer.LayoutParams) {
        return OverScrollContainer.LayoutParams(lp)
    } else if (lp is ViewGroup.MarginLayoutParams) {
        return OverScrollContainer.LayoutParams(lp)
    }
    return OverScrollContainer.LayoutParams(lp)
}

三、自定义 measure 处理

前面也说了,在初期,我们规定该布局只能管理一个可滚动的View(或者ViewGroup),因此,measure 时,只需要看一下第一个布局的宽高,然后让自身宽高等于子布局的宽高即可:

/**
 * 测量子view高度
 */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    if (childCount == 1) {
        //自身尺寸遵循唯一子布局的尺寸
        val child = getChildAt(0)
        val lp = child.layoutParams as LayoutParams
        measureChild(child, widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(child.measuredWidth + lp.marginStart + lp.marginEnd + paddingStart + paddingEnd,
                child.measuredHeight + lp.topMargin + lp.bottomMargin + paddingTop + paddingBottom)
    } else {
        setMeasuredDimension(0, 0)
    }
}

这里设定自身的宽为:child.measuredWidth + lp.marginStart + lp.marginEnd + paddingStart + paddingEnd
设定自身高为:child.measuredHeight + lp.topMargin + lp.bottomMargin + paddingTop + paddingBottom

当子布局不存在或者超过一个,那么自身宽高皆为 0 ,将不会显示出来

四、自定义 layout 处理

measure 之后,我们确定一下唯一子view的上下左右位置。

/**
 * 规范子view的坐标位置
 */
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    val childCount = childCount
    if (childCount == 1) {
        val child = getChildAt(0)
        val lp = child.layoutParams as LayoutParams
        val left_p = lp.marginStart + paddingStart
        val top_p = lp.topMargin + paddingTop
        val right_p = left_p + child.measuredWidth
        val bottom_p = top_p + child.measuredHeight
        child.layout(left_p, top_p, right_p, bottom_p)
    } else {
        //当子布局不存在,或者超过一个,则不进行正常布局
        //setFrame(0,0,0,0);
    }
}

同样的,我们也考虑了paddingmargin对布局造成的影响

五、自定义 draw 处理

在处理draw流程时,需要考虑ScrollXScrollY对布局造成的影响,同时,需要绘制出滚动显示的背景,以及自定义添加的一些文字提示,因此首先定义一些局部变量:


    /**
     * 手指按下的坐标
     * 当前的坐标
     */
    private var down_y: Float = 0.toFloat()
    private var current_y: Float = 0.toFloat()

    /**
     * 控制器
     */
    private var controlListener: ControlInterceptListener? = null

    /**
     * 文字画笔
     */
    private var singlePaint = TextPaint().also {
        it.textSize = 28.toFloat()
        it.color = Color.RED
    }

    /**
     * 平滑滚动控制
     */
    private val singleScroller = Scroller(this.context)

然后根据 scrollY 来绘制背景,scrollY 实际是指该方法的返回值:

/**
 * Return the scrolled top position of this view. This is the top edge of
 * the displayed part of your view. You do not need to draw any pixels above
 * it, since those are outside of the frame of your view on screen.
 *
 * @return The top edge of the displayed part of your view, in pixels.
 */
public final int getScrollY() {
    return mScrollY;
}

根据view的内容滚动原理:

  • scrollY小于0时,表示向下滑动手指,顶部出现滚动区域;
  • scrollY大于0时,表示向上滑动手指;

/**
 * 添加头部的填充内容
 */
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    //存在滚动效果时,进行绘制
    if (scrollY != 0) {
        (getChildAt(0)?.layoutParams as LayoutParams?)?.let { lp ->
            //上部过度滚动效果
            if (scrollY < 0) {
                //绘制默认顶部图形
                lp.bgDrawable.run {
                    setBounds(lp.marginStart + paddingStart, -lp.overScrollDimenTop, width - lp.marginEnd - paddingEnd, lp.topMargin + paddingTop)
                    draw(canvas)
                }

                //绘制文字,保证滑动时,跟随在最顶部
                "你是最棒的!!!".let {
                    singlePaint.color = Color.argb((-1.0 * scrollY / lp.overScrollDimenTop * 0xFF).toInt(), 0xFF, 0x00, 0x00)
                    canvas.drawText(it, (width - singlePaint.measureText(it)) / 2, scrollY - singlePaint.fontMetrics.top, singlePaint)
                }
            }


            //下部过度滚动效果
            if (scrollY > 0) {
                //绘制默认顶部图形
                lp.bgDrawable.run {
                    setBounds(lp.marginStart + paddingStart, height - lp.bottomMargin - paddingBottom, width - lp.marginEnd - paddingEnd, height + lp.overScrollDimenBottom)
                    draw(canvas)
                }

                //绘制文字,保证滑动时,跟随在最顶部
                "我是最棒的!!!".let {
                    singlePaint.color = Color.argb((1.0 * scrollY / lp.overScrollDimenBottom * 0xFF).toInt(), 0xFF, 0x00, 0x00)
                    canvas.drawText(it, (width - singlePaint.measureText(it)) / 2, height + scrollY - singlePaint.fontMetrics.bottom, singlePaint)
                }
            }
        }
    }
}

六、拦截事件,获取坐标信息

在上面的代码中,我们使用了 画笔 singlePaint 等对象,还有一些变量如:手指按压位置坐标当前手指坐标 等数据,在触发滑动操作时,是需要考虑在内的,因此需要不断更新。

当然,还有点击事件的处理,现在不妨考虑的简单一些:所有发生在 OverScrollContainer 上的滑动事件,都会被拦截进行处理。

为了灵活性,我们还向外部提供一个接口,可以控制是否可以拦截事件,不设置监听器的的话默认的话,默认事件会被OverScrollContainer拦截


/**
 * 判断是否可以拦截事件
 */
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    when (ev.action) {
        MotionEvent.ACTION_DOWN -> {
            down_y = ev.y
        }
    }

    //在滚动时,指定所有父布局不能拦截自身的点击事件
    return if (ev.action == MotionEvent.ACTION_MOVE
            && childCount != 0
            && (getChildAt(0).layoutParams as LayoutParams).let { it.bottomEnable or it.topEnable }
            && controlListener?.canIntercept(ev) != false) {
        var temp_parent = parent ?: null
        while (temp_parent != null) {
            parent.requestDisallowInterceptTouchEvent(true)
            temp_parent = temp_parent.parent ?: null
        }
        true
    } else {
        false
    }
}

/**
 * 处理滑动逻辑
 */
override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_MOVE -> {
            current_y = event.y
            val result = (down_y - current_y).toInt()

            (getChildAt(0)?.layoutParams as LayoutParams?)?.let { lp ->
                //判断当前是否可以滑动
                when {
                    result < 0 && lp.topEnable && lp.overScrollDimenTop > 0 -> lp.overScrollDimenTop
                    result > 0 && lp.bottomEnable && lp.overScrollDimenBottom > 0 -> lp.overScrollDimenBottom
                    else -> null
                }?.let {
                    scrollTo(0, if (Math.abs(result) > it) result / Math.abs(result) * it else result)
                }
            }
        }

        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
            //回归原来位置,平滑滚动
            singleScroller.startScroll(scrollX, scrollY, -scrollX, -scrollY, 500)
            invalidate()
        }
    }
    return true
}

然后重写computeScroll方法,保证可以缓慢滑动:


/**
 * smooth-scroll
 */
override fun computeScroll() {
    super.computeScroll()

    //判断动画是否完成
    if (singleScroller.computeScrollOffset()) {
        scrollTo(singleScroller.currX, singleScroller.currY)

        //通知重绘
        invalidate()
    }
}
    

ControlInterceptListener接口代码为:


/**
 * function : 控制OverScrollContainer是否可以拦截事件
 *
 * Created on 2018/10/15  17:44
 * @author mnlin
 */
public interface ControlInterceptListener {
    /**
     * 是否可以拦截
     *
     * @return true表示可以(但实际情况可能并不会去拦截)
     */
    default boolean canIntercept(MotionEvent me){
        return true;
    }
}

这样一来,就可以完成一个简单的回弹效果,效果如下:

在这里插入图片描述

在这基础上,还可以添加一些可以修改字体颜色的属性,或者屏蔽水平竖直滚动的比例来判断,当前滚动发生的方向等等

七、布局文件与类文件

最后贴出使用的xml布局,以及源码

xml文件:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.example.test.tv.test_view.wrapper.OverScrollContainer
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginBottom="20dp"
            android:layout_marginEnd="5dp"
            android:layout_marginStart="5dp"
            android:layout_marginTop="16dp"
            android:background="#5500FFFF"
            android:paddingBottom="14dp"
            android:paddingEnd="10dp"
            android:paddingStart="16dp"
            android:paddingTop="20dp">

            <ImageView
                android:layout_width="300dp"
                android:layout_height="300dp"
                android:layout_margin="10dp"
                android:gravity="center"
                android:src="#22FF4081"
                android:text="456123"
                android:textSize="20sp"
                app:enable_bottom="true"
                app:enable_top="true"
                app:overScrollDimen_bottom="48dp"
                app:overScrollDimen_top="48dp"
                app:overScroll_bg_color="#55aa00aa"/>
        </com.example.test.tv.test_view.wrapper.OverScrollContainer>
    </LinearLayout>
</ScrollView>

OverScrollContainer源码:


/**
 * function : 包裹容器;用于在view外部形成一个可以over-scroll的视图
 *
 *
 * 只能有一个子view,否则,视图将加载错乱
 * 仅支持纵轴过度滑动
 */
class OverScrollContainer : ViewGroup {
    /**
     * 手指按下的坐标
     * 当前的坐标
     */
    private var down_y: Float = 0.toFloat()
    private var current_y: Float = 0.toFloat()

    /**
     * 控制器
     */
    private var controlListener: ControlInterceptListener? = null

    /**
     * 文字画笔
     */
    private var singlePaint = TextPaint().also {
        it.textSize = 28.toFloat()
        it.color = Color.RED
    }

    /**
     * 平滑滚动控制
     */
    private val singleScroller = Scroller(this.context)

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    /**
     * 判断是否可以拦截事件
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                down_y = ev.y
            }
        }

        //在滚动时,指定所有父布局不能拦截自身的点击事件
        return if (ev.action == MotionEvent.ACTION_MOVE
                && childCount != 0
                && (getChildAt(0).layoutParams as LayoutParams).let { it.bottomEnable or it.topEnable }
                && controlListener?.canIntercept(ev) != false) {
            var temp_parent = parent ?: null
            while (temp_parent != null) {
                parent.requestDisallowInterceptTouchEvent(true)
                temp_parent = temp_parent.parent ?: null
            }
            true
        } else {
            false
        }
    }

    /**
     * 处理滑动逻辑
     */
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                current_y = event.y
                val result = (down_y - current_y).toInt()

                (getChildAt(0)?.layoutParams as LayoutParams?)?.let { lp ->
                    //判断当前是否可以滑动
                    when {
                        result < 0 && lp.topEnable && lp.overScrollDimenTop > 0 -> lp.overScrollDimenTop
                        result > 0 && lp.bottomEnable && lp.overScrollDimenBottom > 0 -> lp.overScrollDimenBottom
                        else -> null
                    }?.let {
                        scrollTo(0, if (Math.abs(result) > it) result / Math.abs(result) * it else result)
                    }
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                //回归原来位置,平滑滚动
                singleScroller.startScroll(scrollX, scrollY, -scrollX, -scrollY, 500)
                invalidate()
            }
        }
        return true
    }

    /**
     * smooth-scroll
     */
    override fun computeScroll() {
        super.computeScroll()

        //判断动画是否完成
        if (singleScroller.computeScrollOffset()) {
            scrollTo(singleScroller.currX, singleScroller.currY)

            //通知重绘
            invalidate()
        }
    }

    /**
     * 测量子view高度
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        if (childCount == 1) {
            //自身尺寸遵循唯一子布局的尺寸
            val child = getChildAt(0)
            val lp = child.layoutParams as LayoutParams
            measureChild(child, widthMeasureSpec, heightMeasureSpec)
            setMeasuredDimension(child.measuredWidth + lp.marginStart + lp.marginEnd + paddingStart + paddingEnd,
                    child.measuredHeight + lp.topMargin + lp.bottomMargin + paddingTop + paddingBottom)
        } else {
            setMeasuredDimension(0, 0)
        }
    }

    /**
     * 规范子view的坐标位置
     */
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        val childCount = childCount
        if (childCount == 1) {
            val child = getChildAt(0)
            val lp = child.layoutParams as LayoutParams
            val left_p = lp.marginStart + paddingStart
            val top_p = lp.topMargin + paddingTop
            val right_p = left_p + child.measuredWidth
            val bottom_p = top_p + child.measuredHeight
            child.layout(left_p, top_p, right_p, bottom_p)
        } else {
            //当子布局不存在,或者超过一个,则不进行正常布局
            //setFrame(0,0,0,0);
        }
    }

    /**
     * 添加头部的填充内容
     */
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        //存在滚动效果时,进行绘制
        if (scrollY != 0) {
            (getChildAt(0)?.layoutParams as LayoutParams?)?.let { lp ->
                //上部过度滚动效果
                if (scrollY < 0) {
                    //绘制默认顶部图形
                    lp.bgDrawable.run {
                        setBounds(lp.marginStart + paddingStart, -lp.overScrollDimenTop, width - lp.marginEnd - paddingEnd, lp.topMargin + paddingTop)
                        draw(canvas)
                    }

                    //绘制文字,保证滑动时,跟随在最顶部
                    "你是最棒的!!!".let {
                        singlePaint.color = Color.argb((-1.0 * scrollY / lp.overScrollDimenTop * 0xFF).toInt(), 0xFF, 0x00, 0x00)
                        canvas.drawText(it, (width - singlePaint.measureText(it)) / 2, scrollY - singlePaint.fontMetrics.top, singlePaint)
                    }
                }


                //下部过度滚动效果
                if (scrollY > 0) {
                    //绘制默认顶部图形
                    lp.bgDrawable.run {
                        setBounds(lp.marginStart + paddingStart, height - lp.bottomMargin - paddingBottom, width - lp.marginEnd - paddingEnd, height + lp.overScrollDimenBottom)
                        draw(canvas)
                    }

                    //绘制文字,保证滑动时,跟随在最顶部
                    "我是最棒的!!!".let {
                        singlePaint.color = Color.argb((1.0 * scrollY / lp.overScrollDimenBottom * 0xFF).toInt(), 0xFF, 0x00, 0x00)
                        canvas.drawText(it, (width - singlePaint.measureText(it)) / 2, height + scrollY - singlePaint.fontMetrics.bottom, singlePaint)
                    }
                }
            }
        }
    }

    /**
     * 添加控制器,可以根据子布局的状态来控制OverScrollContainer是否可以滑动
     */
    fun setControlInterceptListener(listener: ControlInterceptListener?) {
        this.controlListener = listener
    }

    override fun generateDefaultLayoutParams(): OverScrollContainer.LayoutParams {
        return OverScrollContainer.LayoutParams(MATCH_PARENT, MATCH_PARENT)
    }

    override fun generateLayoutParams(attrs: AttributeSet): OverScrollContainer.LayoutParams {
        return OverScrollContainer.LayoutParams(context, attrs)
    }

    override fun generateLayoutParams(lp: ViewGroup.LayoutParams): ViewGroup.LayoutParams {
        if (lp is OverScrollContainer.LayoutParams) {
            return OverScrollContainer.LayoutParams(lp)
        } else if (lp is ViewGroup.MarginLayoutParams) {
            return OverScrollContainer.LayoutParams(lp)
        }
        return OverScrollContainer.LayoutParams(lp)
    }

    /**
     * 自定义layoutParams,便于实现自定义的属性
     */
    class LayoutParams : ViewGroup.MarginLayoutParams {
        /**
         * over-scroll的距离
         */
        var overScrollDimenTop: Int = 96
        var overScrollDimenBottom: Int = 96

        /**
         * 是否可以回弹
         */
        var topEnable = true
        var bottomEnable = true

        /**
         * 滚动后留存的背景颜色
         */
        var bgColor = 0
        var bgDrawable: Drawable = ColorDrawable(Color.TRANSPARENT)

        constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
            val a = c.obtainStyledAttributes(attrs, R.styleable.OverScrollContainer)
            overScrollDimenTop = a.getDimensionPixelSize(R.styleable.OverScrollContainer_overScrollDimen_top, 96)
            overScrollDimenBottom = a.getDimensionPixelSize(R.styleable.OverScrollContainer_overScrollDimen_bottom, 96)
            topEnable = a.getBoolean(R.styleable.OverScrollContainer_enable_top, true)
            bottomEnable = a.getBoolean(R.styleable.OverScrollContainer_enable_bottom, true)
            bgColor = a.getColor(R.styleable.OverScrollContainer_overScroll_bg_color, 0)
            bgDrawable = ColorDrawable(bgColor)
            a.recycle()
        }

        constructor(width: Int, height: Int) : super(width, height)

        constructor(source: ViewGroup.LayoutParams) : super(source)

        constructor(source: ViewGroup.MarginLayoutParams) : super(source)

        constructor(source: OverScrollContainer.LayoutParams) : super(source) {
            overScrollDimenTop = source.overScrollDimenTop
            overScrollDimenBottom = source.overScrollDimenBottom
            topEnable = source.topEnable
            bottomEnable = source.bottomEnable
            bgColor = source.bgColor
            bgDrawable = source.bgDrawable
        }
    }
}

2016-07-25 17:00:06 qq_33856343 阅读数 859

在iOS7之后使用导航控制器来管理视图控制器的话,默认是支持左侧滑动返回的,但是如果更改了导航控制器的Item,那么默认左侧返回的功能就失效了。
这里写图片描述
下面介绍如何自定义Item还能支持左侧滑动返回:
1.要自己创建一个继承自UINavigationController的类,并且导航控制器使用它
这里写图片描述

2.在自己创建的导航控制器中添加手势返回

这里写图片描述

这里要打遵守协议,并且要用__weak防止循环引用。

3.在rootViewController中设置手势代理

//遵守<UIGestureRecognizerDelegate>协议
self.navigationController.interactivePopGestureRecognizer.delegate = self;

//调用下面的方法判断是否是rootViewController 如果不判断会有Bug出现
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if (self.navigationController.viewControllers.count == 1)
    {
        return NO;
    }
    else
    {
        return YES;
    }
}

这样就搞定了。

2013-08-02 15:36:38 donny_zhang 阅读数 3852

原创文章,转载请注明出处:http://blog.csdn.net/donny_zhang/article/details/9717631

demo功能:ios UITableView实现单击提示,滑动删除,多选行等操作。iphone 6.1测试通过。

demo说明:DeleteMeController.m;CheckListController.m这些都是用UITableView实现的效果代码。

demo截屏:

   

demo主要代码:


#import "DeleteMeController.h"

@implementation DeleteMeController
@synthesize list;
-(IBAction)toggleEdit:(id)sender {
	[self.tableView setEditing:!self.tableView.editing animated:YES];	
}
#pragma mark -
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
	if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
		// Initialization code
	}
	return self;
}

- (void)viewDidLoad {
	NSString *path = [[NSBundle mainBundle] pathForResource:@"computers" ofType:@"plist"];
	NSMutableArray *array = [[NSMutableArray alloc] initWithContentsOfFile:path];
	self.list = array;
	
	UIBarButtonItem *editButton = [[[UIBarButtonItem alloc]
									initWithTitle:@"Delete"
									style:UIBarButtonItemStyleBordered
									target:self
									action:@selector(toggleEdit:)] autorelease];
	self.navigationItem.rightBarButtonItem = editButton;
	
	[super viewDidLoad];
}


#pragma mark -
#pragma mark Table Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView 
 numberOfRowsInSection:(NSInteger)section {
	return [list count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView 
		 cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	
	static NSString *DeleteMeCellIdentifier = @"DeleteMeCellIdentifier";
	
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:DeleteMeCellIdentifier];
	if (cell == nil) {
		cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero 
									   reuseIdentifier:DeleteMeCellIdentifier] autorelease];
	}
	NSInteger row = [indexPath row];
	cell.text = [self.list objectAtIndex:row];
	return cell;
}

#pragma mark -
#pragma mark Table Delegate Methods
- (void)tableView:(UITableView *)tableView 
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle 
forRowAtIndexPath:(NSIndexPath *)indexPath {
	
	NSUInteger row = [indexPath row];
	[self.list removeObjectAtIndex:row];
	[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] 
					 withRowAnimation:UITableViewRowAnimationFade];
}

@end

demo下载地址:http://download.csdn.net/detail/donny_zhang/5858523
没有更多推荐了,返回首页