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

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

从这个图


图一

点击搜索框到


图二
这个图的时候
  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认识

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 和泓默相识已经两年多了。当时不知道什么原因忽然痴迷于花草园艺,烹饪美食。 我自己寻找到的可能的心理学的解释,事业成...
    繁花坞阅读 819评论 11 18
  • 女猪脚之一: 姓名:沐离玥 年龄:18 身份:六大家族之首,沐离家继承人,同时,是世界第一宫月汐宫宫...
    子蚊阅读 434评论 0 0