美文网首页
京东搜索框的实现(深度定制)

京东搜索框的实现(深度定制)

作者: 简_爱SimpleLove | 来源:发表于2017-06-06 17:12 被阅读227次

    要实现这个搜索框,我们需要先看一下效果,然后再进行拆分,分步实现,这样就没有你想想中的那么难了。

    从这个图


    图一

    点击搜索框到


    图二 这个图的时候
    1. 搜索框被拉伸了,而且有动画的效果
    2. 最右边的扫一扫隐藏,最左边的消息按钮,先是缩小,再然后放大,并且文字改为“取消”
    3. 在第二个图的时候,滑动下面历史记录键盘的时候,键盘隐藏
    4. 再点击取消按钮回到第一个页面的时候,取消按钮缩小,并且放大成为一个有图片的消息按钮,并且搜索框缩短,扫一扫按钮显示

    分析:

    1. 第一个图的搜索框并不是一个textField,而是一个label加上的一个图片,然后再给整个加上了一个手势,当点击的时候进行界面切换(实际上第一个搜索框也没有起到任何的搜索作用,当一点击的时候就跳到了第二图。这告诉我们好多实际上我们看起来是一个东西的事物,其实是两个不同的事物)
    2. 这两个图都并没有用到导航栏,而是将导航栏隐藏了,如果是在导航栏上面进行操作,会有很多不便
    3. 封装了继承自UIViewController的searchController和继承自UITextField的searchBar

    下面说下需要注意的几点:

    注意1

    封装button,一种是没有图片的,一种是有图片和文字的,图片在上,文字在下,这也相当于是特别定制的button

    #import <UIKit/UIKit.h>
    
    @interface EOCButton : UIButton
    @property(nonatomic, assign)BOOL noImage;
    @end
    
    #import "EOCButton.h"
    
    @implementation EOCButton
    
    - (void)layoutSubviews {
        [super layoutSubviews];
        // 当没有设置noImage这个属性的时候,默认noImage为nil,即有图片
        if (!_noImage) {
            self.imageView.frame = CGRectMake(6.f, 0.f, self.eoc_width-12.f, self.eoc_width-12.f);
            self.titleLabel.frame = CGRectMake(0.f, self.eoc_height-12.f, self.eoc_width, 12.f);
            self.titleLabel.font = [UIFont systemFontOfSize:9.f];
        } else {
            NSLog(@"no image");
            self.titleLabel.frame = CGRectMake(0.f, 0.f, self.eoc_width, self.eoc_height);
            self.titleLabel.font = [UIFont systemFontOfSize:14.f];
        }
        self.titleLabel.textAlignment = NSTextAlignmentCenter;
        //纵横适配,使一直保持居中
        self.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
        self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
    //    [self setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
    }
    @end
    
    注意2

    封装上面这一块,相当于导航栏,导航栏的高度

    #import <UIKit/UIKit.h>
    #import "EOCButton.h"
    
    @interface EOCNavigationView : UIView
    typedef void (^btnBlock)();
    @property(nonatomic, strong)UIImageView *searchView;
    @property(nonatomic, strong)EOCButton *messageBtn;
    @property(nonatomic, strong)EOCButton *scanBtn;
    @property(nonatomic, strong)UIButton *audioBtn;
    @property(nonatomic, strong)btnBlock scanActionBlock;
    @property(nonatomic, strong)btnBlock audioActionBlock;
    @property(nonatomic, strong)btnBlock searchActionBlock;
    @property(nonatomic, strong)btnBlock messageActionBlock;
    @end
    
    #import "EOCNavigationView.h"
    
    @implementation EOCNavigationView
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        if (self = [super initWithFrame:frame]) {
            self.backgroundColor = [UIColor clearColor];
            [self setNavigationView];
        }
        return self;
    }
    
    - (void)setNavigationView
    {
        
        /*
         这里searchView、button的frame是根据Debug view Hierarchy 查出取消按钮和搜索框的间距,以及搜索框高度来计算出来的
         */
        
        //创建searchBar 把它添加到UINavigationBar的titleView里
        _searchView = [[UIImageView alloc] initWithFrame:CGRectMake(50.f, 8.f, SCREENSIZE.width-103.f, 28.f)];
        _searchView.userInteractionEnabled = YES;
        UIImage *searchBg = [UIImage imageNamed:@"searchBar_white"];
        searchBg = [searchBg stretchableImageWithLeftCapWidth:30.f topCapHeight:0.f]; //左边30不拉伸
        _searchView.image = searchBg;
        
        //创建placeHolder
        UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectMake(30.f, 0.f, _searchView.frame.size.width-30.f, _searchView.frame.size.height)];
        textLabel.backgroundColor = [UIColor clearColor];
        textLabel.textAlignment = NSTextAlignmentLeft;
        textLabel.textColor = [UIColor whiteColor];
        textLabel.font = [UIFont systemFontOfSize:14.0f];
        textLabel.text = @"八点钟学院";
        [_searchView addSubview:textLabel];
        
        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(searchAction)];
        [_searchView addGestureRecognizer:tapGesture];
        [self addSubview:_searchView];
        
        //添加语音按钮
        _audioBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        UIImage *audioImage = [UIImage imageNamed:@"voice_white"];
        CGFloat audioBtnWidth = 12.f;
        CGFloat audioBtnHeight = audioImage.size.height*audioBtnWidth/audioImage.size.width;
        _audioBtn.frame = CGRectMake(_searchView.frame.size.width-20.f, (30-audioBtnHeight)/2, audioBtnWidth, audioBtnHeight);
        [_audioBtn setBackgroundImage:audioImage forState:UIControlStateNormal];
        [_audioBtn addTarget:self action:@selector(audioAction) forControlEvents:UIControlEventTouchUpInside];
        [_searchView addSubview:_audioBtn];
        
        
        //创建扫描按钮
        _scanBtn = [EOCButton buttonWithType:UIButtonTypeCustom];
        _scanBtn.frame = CGRectMake(10.f, 7.f, 30.f, 33.f);
        [_scanBtn setTitle:@"扫一扫" forState:UIControlStateNormal];
        [_scanBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [_scanBtn setImage:[UIImage imageNamed:@"scan_white"] forState:UIControlStateNormal];
        [_scanBtn addTarget:self action:@selector(scanAction) forControlEvents:UIControlEventTouchUpInside];
        [self addSubview:_scanBtn];
        
        //创建消息按钮
        _messageBtn = [EOCButton buttonWithType:UIButtonTypeCustom];
        _messageBtn.frame = CGRectMake(SCREENSIZE.width-40.f, 7.f, 30.f, 33.f);
        [_messageBtn setImage:[UIImage imageNamed:@"message_white"] forState:UIControlStateNormal];
        [_messageBtn setTitle:@"消息" forState:UIControlStateNormal];
        [_messageBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [_messageBtn addTarget:self action:@selector(messageAction) forControlEvents:UIControlEventTouchUpInside];
        [self addSubview:_messageBtn];
        
    }
    
    #pragma mark audio&&scan Button action
    - (void)scanAction
    {
        if (_scanActionBlock) {
            _scanActionBlock();
        }
    }
    
    - (void)audioAction
    {
        if (_audioActionBlock) {
            _audioActionBlock();
        }
    }
    
    - (void)messageAction
    {
        if (_messageActionBlock) {
            _messageActionBlock();
        }
    }
    
    #pragma mark - tapGesture Action
    - (void)searchAction
    {
        if (_searchActionBlock) {
            _searchActionBlock();
        }
    }
    @end
    

    需要注意的是,这里使用了block来传递按钮的点击事件,非常方便,只需要在创建的时候,这样调用

    _navigationView = [[EOCNavigationView alloc] initWithFrame:CGRectMake(0.f, 20.f, self.view.eoc_width, 44.f)];
            //创建navView的scanAction、audioAction、messageAction、searchAction的block
            __weak typeof(self)weakSelf = self;
            _navigationView.scanActionBlock = ^{
            };
            _navigationView.audioActionBlock = ^{
            };
            _navigationView.messageActionBlock = ^{
            };
            _navigationView.searchActionBlock = ^{
                [weakSelf goToSearchPage];
            };
    

    然后就是使用了searchBg = [searchBg stretchableImageWithLeftCapWidth:30.f topCapHeight:0.f]; //左边30不拉伸这个方法第一个参数是左边不拉伸区域,第二个参数是上边不拉伸区域。

    注意3

    在封装继承自UIViewController的searchController中,在初始化方法中需要先设置frame,不然会显示白屏,_searchResultViewController添加不上

    - (instancetype)initWithSearchResultControlloer:(UIViewController *)resultViewController {
        
        if ([super init]) {
            if (resultViewController) {
                //添加_searchResultViewController是添加在self.view上面,所以要先设置frame, 不然就是白屏
                self.view.frame = CGRectMake(0.f, 64.f, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height-64.f);
                _searchResultViewController = (EOCSearchResultViewCtrl *)resultViewController;
                _searchResultViewController.view.frame = CGRectMake(0.f, 0.f, self.view.eoc_width, self.view.eoc_height);
                //添加到我们的EOCClassSearchController上来
                [self addChildViewController:_searchResultViewController];
            }
        }
        return self;
    }
    
    注意4

    在封装继承自UIViewController的searchController中,监听searchBar的输入不能用- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string这个方法,因为这样会使当输入第一个字符的时候,没有搜索结果展示,只有当输入第二个字符的时候才会走这个方法,出来搜索结果


    所以要想第一个字符也能监听到,要使用[_searchBar addTarget:self action:@selector(textChange) forControlEvents:UIControlEventEditingChanged];这个方法
    - (void)textChange {  // 使用这个方法一开始输入搜索字符的时候,下面的view就添加上了
        if (_delegate && self.searchBar.text.length == 1) {
            
            //添加resultViewController的view;同时我把searachController添加到另外一个viewcontroller
            if (!self.parentViewController) {   // 防止当删除到只剩一个元素的时候也调用这段代码
                
                [self.view addSubview:_searchResultViewController.view];
                //添加searachController到mainSearchCtrl上
                [_delegate didPresentSearchController:self];
            }
        }
        
        // 当搜索关键字删除完了过后,要退出搜索结果界面
        if (_delegate && self.searchBar.text.length == 0) {
            [_searchResultViewController.view removeFromSuperview];
            [_delegate didDismissSearchController:self];
        }
        // 当更新的代理存在的时候,随时更新搜索内容
        if (_searchResultUpdater) {
            [_searchResultUpdater updateSearchResultsForSearchController:self];
        }
        
    }
    
    注意5

    当从最上面所说的第一个图到第二个图的时候要使用方法

        EOCClassMainSearchCtrl *searchCtrl = [[EOCClassMainSearchCtrl alloc] init];
        [self addChildViewController:searchCtrl];
        [self.view addSubview:searchCtrl.view];
    //    [self presentViewController:searchCtrl animated:NO completion:^{
    //        
    //    }];
    

    当点击取消按钮从第二个图到第一个图的时候使用下面的方法

    //    [self dismissViewControllerAnimated:NO completion:^{
    //    }];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"searchBarRemove" object:nil];
        [self removeFromParentViewController];
        [self.view removeFromSuperview];
    

    不能用上面注释的方法,不然会有很明显的延迟的效果



    而且当点击取消按钮的时候还要发送通知,在第一个图的界面实现搜索框变短,按钮从小到大的动画效果。

    注意6

    在封装继承自UIViewController的searchController中需要定义两个代理协议,一个用于输入搜索关键字过后进行更新,一个用于展示和退出搜索结果界面。

    #import <UIKit/UIKit.h>
    #import "EOCClassSearchBar.h"
    #import "EOCSearchResultViewCtrl.h"
    
    @class EOCClassSearchController;
    
    @protocol EOCSearchResultsUpdating <NSObject>
    @required
    // Called when the search bar's text or scope has changed or when the search bar becomes first responder.
    - (void)updateSearchResultsForSearchController:(EOCClassSearchController *)searchController;
    @end;
    
    @protocol EOCSearchControllerDelegate <NSObject>
    - (void)didPresentSearchController:(EOCClassSearchController *)searchController;
    - (void)didDismissSearchController:(EOCClassSearchController *)searchController;
    @end
    
    
    @interface EOCClassSearchController : UIViewController
    
    @property(nonatomic, strong)EOCClassSearchBar *searchBar;
    @property(nonatomic, strong)EOCSearchResultViewCtrl *searchResultViewController;
    @property(nonatomic, weak)id <EOCSearchResultsUpdating> searchResultUpdater;
    @property(nonatomic, weak)id <EOCSearchControllerDelegate> delegate;
    
    - (instancetype)initWithSearchResultControlloer:(UIViewController *)resultViewController;
    @end
    
    #pragma mark - EOCSearchResultsUpdating delegate
    - (void)updateSearchResultsForSearchController:(EOCClassSearchController *)searchController {
    
        NSString *searchText = searchController.searchBar.text;
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(SELF CONTAINS %@)", searchText];
        EOCSearchResultViewCtrl *resultViewCtrl = (EOCSearchResultViewCtrl *)searchController.searchResultViewController;
        NSArray *dataArr = [EOCDataModel sharedDataModel].dataArr;
        resultViewCtrl.filterDataArr = [dataArr filteredArrayUsingPredicate:predicate];
    }
    
    #pragma mark - EOCSearchController delegate
    - (void)didPresentSearchController:(EOCClassSearchController *)searchController {
        [self addChildViewController:searchController];
        [self.view addSubview:searchController.view];
    }
    
    - (void)didDismissSearchController:(EOCClassSearchController *)searchController {
        [searchController removeFromParentViewController];
        [searchController.view removeFromSuperview];
    }
    
    注意7

    [self.searchController.searchBar becomeFirstResponder];这段代码要放在主界面的viewDidLoad方法中,防止出现卡顿

    @implementation EOCClassMainSearchCtrl
    
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        
        [self createNavigationView];
        [self.view addSubview:self.searchHistoryTable];
        //放在这里防止一开始会出现卡顿的效果
        [self.searchController.searchBar becomeFirstResponder];
    }
    

    需要注意的是在模拟器上还是会出现如下的卡顿情况,但是在真机上不会

    注意8

    按钮动画效果的实现

    //页面完全显示
    - (void)viewDidAppear:(BOOL)animated {
        
        [super viewDidAppear:animated];
        [UIView animateWithDuration:0.2f animations:^{
        self.searchController.searchBar.frame = CGRectMake(10.f, 8.f, SCREENSIZE.width-63.f, 28.f);
    //        _scanBtn.alpha = 0.f;
            _scanBtn.hidden = YES;
        } completion:^(BOOL finished) {
        }];
        //basicAnimation来实现取消按钮的动画效果
        CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
        basicAnimation.fromValue = @1;
        basicAnimation.toValue = @0;
        basicAnimation.duration = 0.2f;
        basicAnimation.delegate = self;
        
        basicAnimation.fillMode = kCAFillModeForwards;  // 保持前面动画的值,即就是缩到最小0的状态,然后再放大,防止缩小到0的状态后,恢复原状,再从0到1逐渐放大
        basicAnimation.removedOnCompletion = NO;
        
        [_messageBtn.layer addAnimation:basicAnimation forKey:nil];
        
        //不加下面这句代码延迟,会因为basicAnimation.removedOnCompletion = NO;这句代码造成EOCClassMainSearchCtrl不能被销毁,从而内存泄漏
        [self performSelector:@selector(removeanimtion) withObject:nil afterDelay:1.f];
        
    }
    
    - (void)removeanimtion {
        
        [_messageBtn.layer removeAllAnimations];
        
    }
    //动画代理方法,当动画结束后调用
    - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
        
        //修改message button的文字
        _messageBtn.noImage = YES;
        [_messageBtn setImage:nil forState:UIControlStateNormal];
        [_messageBtn setTitle:@"取消" forState:UIControlStateNormal];
        [_messageBtn setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
        [_messageBtn addTarget:self action:@selector(messageAction) forControlEvents:UIControlEventTouchUpInside];
        //basicAnimation来实现
        CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
        basicAnimation.fromValue = @0;
        basicAnimation.toValue = @1;
        basicAnimation.duration = 0.2f;
        
        basicAnimation.fillMode = kCAFillModeForwards;
        basicAnimation.removedOnCompletion = NO;
        
        [_messageBtn.layer addAnimation:basicAnimation forKey:nil];
        
    }
    

    先在第一个方法中,即页面完全显示的时候先将按钮从1到0缩小,然后动画结束后再执行动画从0到1放大,并且改变文字为“取消”。
    kCAFillModeForwards:当动画结束后,layer会一直保持着动画最后的状态
    removedOnCompletion:默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode为kCAFillModeForwards

    注意9

    关于动画中会影响内存泄漏的问题:在上面的注意8中如果不在动画结束过后,移除动画,会造成控制器无法被释放,从而造成内存泄漏,所以必须要加上下面这句代码

    //不加下面这句代码延迟,会因为basicAnimation.removedOnCompletion = NO;这句代码造成EOCClassMainSearchCtrl不能被销毁,从而内存泄漏
        [self performSelector:@selector(removeanimtion) withObject:nil afterDelay:1.f];
    
    - (void)removeanimtion {
        [_messageBtn.layer removeAllAnimations];
    }
    
    注意10

    要实现在第二个图的时候,滑动下面历史记录键盘的时候,键盘隐藏,其中下面的历史记录是封装的一个tableView,所以要将滚动事件传递出来,这里用到了block,当然也可以用代理和通知,但是个人认为用block要方便一些

    #import <UIKit/UIKit.h>
    
    typedef void(^scrollActionBlock) ();
    @interface EOCSearchTable : UITableView<UITableViewDelegate, UITableViewDataSource>
    @property(nonatomic, strong)NSArray *searchHistoryArr;
    @property(nonatomic, strong)scrollActionBlock scrollBlock;
    @end
    
    -(void)scrollViewDidScroll:(UIScrollView *)scrollView {
        if (_scrollBlock) {
            _scrollBlock();
        }
    }
    

    然后在使用tableView的地方,实现block方法,使其不为空就好

            //创建搜索记录tableView
            _searchHistoryTable = [[EOCSearchTable alloc] initWithFrame:CGRectMake(0.f, 64.f, SCREENSIZE.width, SCREENSIZE.height-64.f) style:UITableViewStyleGrouped];
            __weak typeof(_searchController)weakSearch = _searchController;
            _searchHistoryTable.scrollBlock = ^{
                [weakSearch.searchBar resignFirstResponder];
            };
    

    block默认会将外部的变量拷贝一份,在block中如果要对外部参数进行修改就要用__block,如果不修改只是用__weak就够了

    本文为我在腾讯课堂八点钟学院学习所做的笔记

    参考文章
    UIImage部分拉伸——stretchableImageWithLeftCapWidth的使用
    iOS开发基础知识:Core Animation(核心动画)
    (iOS)__block和__weak认识

    相关文章

      网友评论

          本文标题:京东搜索框的实现(深度定制)

          本文链接:https://www.haomeiwen.com/subject/naisfxtx.html