JQWord 02(卡片模块)

JQCardViewController

#import "JQCardController.h"
#import "JQCardSwitchView.h"
#import "JQWordTool.h"
#import "JQCardView.h"
#import "JQEditController.h"
#import "JQNavigationController.h"
#import "JQAddWordController.h"

@interface JQCardController ()
@property (nonatomic,strong)JQCardSwitchView *cardSwitchView;
@property (nonatomic,strong)NSArray *words;
@property (nonatomic,assign)NSInteger editViewtag;//正在编辑view的tag

@end

@implementation JQCardController

#pragma mark 生命周期方法

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setAutomaticallyAdjustsScrollViewInsets:NO];//关闭对其子控件scrollView的默认内边距
#warning 如果navigationBar设置了背景图片,那么这句也没用
    
    self.view.backgroundColor=[UIColor whiteColor];

    //添加右边+号按钮
    self.navigationItem.rightBarButtonItem=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addClick)];
    
    //添加switchView
    [self addCardSwitchView];
}

- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    [self.cardSwitchView updateWords];//告诉switchView可以更新数据了
}

#pragma mark 初始方法
/**
 *  添加switchView
 */
- (void)addCardSwitchView {
    CGFloat cardSwitchViewH=420;
    CGFloat cardSwitchViewY=(self.view.height-cardSwitchViewH)*0.5;
    CGRect cardSwitchViewRect=CGRectMake(0, cardSwitchViewY, JQMainScreenSize.width, cardSwitchViewH);
    
    //设置frame
    _cardSwitchView = [[JQCardSwitchView alloc]initWithFrame:cardSwitchViewRect];
    _cardSwitchView.backgroundColor=[UIColor clearColor];
    [self.view addSubview:_cardSwitchView];
}

#pragma mark 点击方法
/**
 *  点击+号按钮
 */
- (void)addClick{
    JQAddWordController *addWordController=[[JQAddWordController alloc]init];
    JQNavigationController *navVC=[[JQNavigationController alloc]init];
    [navVC addChildViewController:addWordController];
    
    [self presentViewController:navVC animated:YES completion:nil];
}

JQCardSwitchView

#import "JQCardSwitchView.h"
#import "JQWord.h"
#import "JQCardView.h"
#import "JQCardController.h"
#import "JQNavigationController.h"
#import "JQEditController.h"

@interface JQCardSwitchView () <UIScrollViewDelegate>
@property (nonatomic,strong)NSArray *words; //创建子控件需要的总数据
@property (nonatomic,strong)NSMutableSet *reuseViewSet;//装当前可重用view
@property (nonatomic,strong)NSMutableArray *visibleViewArr;//装当前正显示的view
@property (nonatomic,assign)CGSize subSize; //一个子控件的size
@end

@implementation JQCardSwitchView{
    JQCardView *_curCardView;
    CGFloat _basicSpace; //基础距离 (计算第一个子控件与左边框之间的距离)
    CGFloat _subSpace; //子控件之间距离
    NSInteger _visibleCount; //初始显示时可显示子控件数
}

#pragma mark 懒加载
- (NSMutableSet *)reuseViewSet{
    if (!_reuseViewSet) {
        _reuseViewSet=[NSMutableSet set];
    }
    return _reuseViewSet;
}

- (NSMutableArray *)visibleViewArr{
    if (!_visibleViewArr){
        _visibleViewArr=[NSMutableArray array];
    }
    return _visibleViewArr;
}

#pragma mark 公开方法
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.delegate=self;
        self.subSize=CGSizeMake(240, self.height);
        #warning 这里是自己定义的subSize
        
        self.showsHorizontalScrollIndicator=NO;
        [self updateWords];
    }
    return self;
}

/**
 *  更新数据:(调用该函数后,会进入setWords重写函数,会重新初始化子控件)
 */
- (void)updateWords{
    self.words=[JQWordTool getAllWords];
}

#pragma mark 重写方法
- (void)setSubSize:(CGSize)subSize{
    _subSize=subSize;
    
    //1.计算visibleCount/subSpace/basicSize
    int count=self.width/subSize.width;//当前屏幕可以摆放子控件的个数
    _basicSpace=self.width-(count*subSize.width);
    if(_basicSpace>0){    //只要有间距,那么其右边默认可以多显示一个子控件
        _visibleCount=count+1;
        _subSpace=_basicSpace/(count+3);//计算出子控件距离
        _basicSpace = _subSpace * 2;
    }else{
        _visibleCount=count;
        _subSpace = 0;
        _basicSpace = 0;
    }
}

/**
 *  拦截数据设置 创建初始view
 */
- (void)setWords:(NSArray *)words{
    _words=words;
    
    //清空操作
    //->移除当前view上的所有子控件
    [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
    //->移除可视数组
    [self.visibleViewArr removeAllObjects];
    //->移除重用集合
    [self.reuseViewSet removeAllObjects];
    
    //1.创建初始子控件
    NSInteger count=(self.words.count<_visibleCount?self.words.count:_visibleCount);
    for(int i=0;i<count;i++){
        
        JQCardView *cardView=[[JQCardView alloc]initWithFrame:CGRectMake(0, 0, self.subSize.width, self.subSize.height)];
        
        //->设置tag
        cardView.tag=i;
        
        //->通用操作
        [self setupCardView:cardView];
        
        //2.添加到可视数组
        [self.visibleViewArr addObject:cardView];//相当于放到数组最后面
    }
    
    //3.做一次形变
    [self transshape];
    
    //4.计算contentSize
    CGFloat lastX=[self getXWithTag:(self.words.count-1)];
    CGSize size=CGSizeMake(lastX+self.subSize.width+(2*_subSpace), self.height);
    self.contentSize=size;
    
    //5.偏移处理
    [self setupOffset];
}

#pragma mark UIScrollViewDelegate 代理方法
/**
 *  scrollView滚动
 */
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    //0.超出范围不做任何操作
    CGFloat offsetMinX=self.contentOffset.x;
    CGFloat offsetMaxX=offsetMinX+self.width;
    if (offsetMinX<=0 || offsetMaxX>=self.contentSize.width) {
        return;
    }

    //1.回收子控件
    [self retrieveView];
    
    //2.创建欲显示子控件
    [self createNewView];
 
    //3.变形
    [self transshape];
}

#pragma mark 逻辑方法
/**
 *  回收子控件
 */
- (void)retrieveView{
    //1.当前scrollView边框(可视边框)X
    CGFloat offsetMinX=self.contentOffset.x;
    CGFloat offsetMaxX=offsetMinX+self.width;
    
    //2.得出第一个和最后一个可视view的X
    //->拿出第一个和最后一个可视view
    UIView *firstView=self.visibleViewArr.firstObject;
    UIView *lastView=self.visibleViewArr.lastObject;

    CGFloat firstViewMaxX=firstView.x+self.subSize.width;;
    CGFloat lastViewX=lastView.x;
    
    //3.判断是否第一个可视view消失在屏幕中
    if (firstViewMaxX<=offsetMinX) {
        //添加到回收容器
        //->还原待回收view的形变属性
        firstView.transform=CGAffineTransformIdentity;
        [self.reuseViewSet addObject:firstView];
        //从当前可视viewArr中移除
        [self.visibleViewArr removeObject:firstView];
        //从当前view中移除
        [firstView removeFromSuperview];
        
    }
    
    //3.判断是否最后一个可视view消失在屏幕中
    if (lastViewX>=offsetMaxX) {
        //添加到回收容器
        lastView.transform=CGAffineTransformIdentity;
        [self.reuseViewSet addObject:lastView];
        //从当前可视viewArr中移除
        [self.visibleViewArr removeObject:lastView];
        //从当前view中移除
        [lastView removeFromSuperview];
        
    }
    
}

/**
 *  创建欲显示子控件
 */
- (void)createNewView{
    //1.当前scrollView边框(可视边框)X
    CGFloat offsetMinX=self.contentOffset.x;
    CGFloat offsetMaxX=offsetMinX+self.width;
    
    //2.计算可能会显示的view的X
    //->计算第一个可视view的左边view的MaxX
    UIView *firstView=self.visibleViewArr.firstObject;
    CGFloat preViewMinX=[self getXWithTag:firstView.tag-1];
    CGFloat preViewMaxX=preViewMinX+self.subSize.width;
    
    //->计算最后一个可视view的右边view的minX
    UIView *lastView=self.visibleViewArr.lastObject;
    CGFloat afterViewX=[self getXWithTag:lastView.tag+1];
    
    //3.添加即将显示的view
    //->判断条件
    BOOL leftJudge=(offsetMinX<preViewMaxX && firstView.tag>0);//不能在0之前加入view
    BOOL rightJudge=(offsetMaxX>afterViewX && lastView.tag<(self.words.count-1));
    
    if (leftJudge || rightJudge) {
        
        JQCardView *cardView=[self.reuseViewSet anyObject];
        if (cardView) { //判断是否可以重用
            [self.reuseViewSet removeObject:cardView];
        }else{
            cardView=[[JQCardView alloc]initWithFrame:CGRectMake(0, 0, self.subSize.width, self.subSize.height)];
        }
        
        //->左边添加一个view
        if (leftJudge) {
            cardView.tag=firstView.tag-1;
            [self.visibleViewArr insertObject:cardView atIndex:0];
        }
        //右边添加一个view
        else{
            cardView.tag=lastView.tag+1;
            [self.visibleViewArr addObject:cardView];
        }
        
        //->添加cardView的通用设置
        [self setupCardView:cardView];
    }
}

/**
 *  形变方法 (scrollView在滚动的时候,可能里面的子控件需要变形)
 */
- (void)transshape{
    
    CGFloat offsetMinX=self.contentOffset.x;
    
    for (JQCardView *view  in self.visibleViewArr) {
        CGFloat width = view.width + _subSpace;
        CGFloat y = view.tag * width;
#warning 一定是要用tag哦!只有用自身的tag才能得到自身本来该有的位置
        CGFloat value = (offsetMinX-y)/width;
        CGFloat scale = fabs(cos(fabs(value)*M_PI/5));
        view.transform = CGAffineTransformMakeScale(1.0, scale);
    }
}

#pragma mark 其他方法
/**
 *  根据tag算出x
 */
- (CGFloat)getXWithTag:(NSInteger)tag{
    return _basicSpace+(_subSize.width+_subSpace)*tag;
    /*根据第一第二个子控件的x可以推出来*/
}

/**
 *  设置回到上次偏移
 */
- (void)setupOffset{
    CGPoint offset = self.contentOffset;
    for(int i=0;i<=offset.x;i++){
        [self setContentOffset:CGPointMake(i, 0)];
    }
    /*
     为什么不直接设置偏移量到offset处呢?
     因为每个点这样的偏移会调用若干次 scrollView代理方法
     就可以实时的计算出可回收view和创建新view
     */
}

/**
 *  cardView创建过程中,通用设置
 */
- (void)setupCardView:(JQCardView *)cardView{
    //1.添加向上扫动手势
    UISwipeGestureRecognizer *gesture = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(cardViewSwipe:)];
    gesture.direction = UISwipeGestureRecognizerDirectionUp;
    [cardView addGestureRecognizer:gesture];
    
    //2.基础配置
    //->重设x
    cardView.x=[self getXWithTag:cardView.tag];//得到x
    //->设置数据
    cardView.word=_words[cardView.tag];
    cardView.editTitle=@"编辑";

    __weak typeof(cardView) tempView=cardView;
    cardView.editAction=^(){
        JQCardController *cardVC=(JQCardController *)tempView.parentViewController;
        
        JQNavigationController *navVC=[[JQNavigationController alloc]init];
        JQEditController *editVC=[[JQEditController alloc]init];
        editVC.word=tempView.word;
        [navVC addChildViewController:editVC];
        
        [cardVC presentViewController:navVC animated:YES completion:nil];
    };
    
    //3.添加
    [self addSubview:cardView];
}

/**
 *  cardView手势触发方法
 */
- (void)cardViewSwipe:(UISwipeGestureRecognizer *)gesture{
    JQCardView *cardView=(JQCardView *)gesture.view;
    
    //弹出提示框;
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:@"确定要删除?" preferredStyle: UIAlertControllerStyleActionSheet];
    
    [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        
        [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
            cardView.y = -(cardView.height);
        } completion:^(BOOL finished) {
            //1.删除数据
            [JQWordTool deleteWord:cardView.word];
            
            //2.回收cardView
            if (self.reuseViewSet.count<=0) {
                [self.reuseViewSet addObject:cardView];
            }
            
            //3.删除cardView
            [self.visibleViewArr removeObject:cardView];
            [cardView removeFromSuperview];
            
            //4.更新当前数据
            [self updateWords];
        }];
        
    }]];
    
    [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        //点击按钮的响应事件;
    }]];
    
    [cardView.parentViewController presentViewController:alert animated:YES completion:nil];
    
}

@end

JQCardView原理

Paste_Image.png
Paste_Image.png

1.设置一个卡片的宽高

self.subSize=CGSizeMake(240, self.height);

2.计算出其他参数

/*
     _basicSpace基础距离
     _visibleCount初始显示时,可显示控件个数
     _subSpace子控件之间的距离
     space 屏幕宽度-所有控件宽度和
     */
    //1.计算visibleCount/subSpace/basicSize
    int count=self.width/subSize.width;//当前屏幕可以摆放子控件的个数
    CGFloat space=self.width-(count*subSize.width);
    if(space>0){    //只要有间距,那么其右边默认可以多显示一个子控件
        _visibleCount=count+1;
        _subSpace=space/(count+3);//计算出子控件距离
        _basicSpace = _subSpace * 2;
    }
    //->刚好能摆放若干子控件
    else{
        _visibleCount=count;
        _subSpace = 0;
        _basicSpace = 0;
    }
    /*
     _visibleCount=count+1解释:
     space>0,一个控件左右一定可以摆放其他控件,因为是初始显示
     所以只有右边摆放控件所以count+1
     同理,如果count==2,那么_visibleCount==3
     
     _subSpace=space/(count+3)解释:
     当:space>0,count == 1
     那么该控件的左右两边一定可以显示其他控件
     那么space就要平分为4份 _subSpace = space/4
     同理,如果count == 2
     _subSpace = space/5
     */

所以每个控件的x计算方式可以推出:


viewX = _basicSpace+(_subSize.width+_subSpace)*view.tag;

3.移动时回收和创建
回收:

  • 当第一个可视view的最大X消失在屏幕左边,那么可以回收该view
  • 当最后一个可视view的最小X消失屏幕右边,那么可以回收该view

/**
 *  回收子控件
 */
- (void)retrieveView{
    //1.当前scrollView边框(可视边框)X
    CGFloat offsetMinX=self.contentOffset.x;
    CGFloat offsetMaxX=offsetMinX+self.width;
    
    //2.得出第一个和最后一个可视view的X
    //->拿出第一个和最后一个可视view
    UIView *firstView=self.visibleViewArr.firstObject;
    UIView *lastView=self.visibleViewArr.lastObject;

    CGFloat firstViewMaxX=firstView.x+self.subSize.width;;
    CGFloat lastViewX=lastView.x;
    
    //3.判断是否第一个可视view消失在屏幕中
    if (firstViewMaxX<=offsetMinX) {
        //添加到回收容器
        //->还原待回收view的形变属性
        firstView.transform=CGAffineTransformIdentity;
        [self.reuseViewSet addObject:firstView];
        //从当前可视viewArr中移除
        [self.visibleViewArr removeObject:firstView];
        //从当前view中移除
        [firstView removeFromSuperview];
        
    }
    
    //3.判断是否最后一个可视view消失在屏幕中
    if (lastViewX>=offsetMaxX) {
        //添加到回收容器
        lastView.transform=CGAffineTransformIdentity;
        [self.reuseViewSet addObject:lastView];
        //从当前可视viewArr中移除
        [self.visibleViewArr removeObject:lastView];
        //从当前view中移除
        [lastView removeFromSuperview];
        
    }
    
}

创建:

  • 当第一个可视view的左边view的最大X开始出现在屏幕左边时可以创建该view
  • 当最后一个可视view的右边view的最小X开始出现在屏幕右边时可以创建该view
/**
 *  创建欲显示子控件
 */
- (void)createNewView{
    //1.当前scrollView边框(可视边框)X
    CGFloat offsetMinX=self.contentOffset.x;
    CGFloat offsetMaxX=offsetMinX+self.width;
    
    //2.计算可能会显示的view的X
    //->计算第一个可视view的左边view的MaxX
    UIView *firstView=self.visibleViewArr.firstObject;
    CGFloat preViewMinX=[self getXWithTag:firstView.tag-1];
    CGFloat preViewMaxX=preViewMinX+self.subSize.width;
    
    //->计算最后一个可视view的右边view的minX
    UIView *lastView=self.visibleViewArr.lastObject;
    CGFloat afterViewX=[self getXWithTag:lastView.tag+1];
    
    //3.添加即将显示的view
    //->判断条件
    BOOL leftJudge=(offsetMinX<preViewMaxX && firstView.tag>0);//不能在0之前加入view
    BOOL rightJudge=(offsetMaxX>afterViewX && lastView.tag<(self.words.count-1));
    
    if (leftJudge || rightJudge) {
        
        JQCardView *cardView=[self.reuseViewSet anyObject];
        if (cardView) { //判断是否可以重用
            [self.reuseViewSet removeObject:cardView];
        }else{
            cardView=[[JQCardView alloc]initWithFrame:CGRectMake(0, 0, self.subSize.width, self.subSize.height)];
        }
        
        //->左边添加一个view
        if (leftJudge) {
            cardView.tag=firstView.tag-1;
            [self.visibleViewArr insertObject:cardView atIndex:0];
        }
        //右边添加一个view
        else{
            cardView.tag=lastView.tag+1;
            [self.visibleViewArr addObject:cardView];
        }
        
        //->添加cardView的通用设置
        [self setupCardView:cardView];
    }
}
animation.gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容