要实现这个搜索框,我们需要先看一下效果,然后再进行拆分,分步实现,这样就没有你想想中的那么难了。
从这个图
点击搜索框到
- 搜索框被拉伸了,而且有动画的效果
- 最右边的扫一扫隐藏,最左边的消息按钮,先是缩小,再然后放大,并且文字改为“取消”
- 在第二个图的时候,滑动下面历史记录键盘的时候,键盘隐藏
- 再点击取消按钮回到第一个页面的时候,取消按钮缩小,并且放大成为一个有图片的消息按钮,并且搜索框缩短,扫一扫按钮显示
分析:
- 第一个图的搜索框并不是一个textField,而是一个label加上的一个图片,然后再给整个加上了一个手势,当点击的时候进行界面切换(实际上第一个搜索框也没有起到任何的搜索作用,当一点击的时候就跳到了第二图。这告诉我们好多实际上我们看起来是一个东西的事物,其实是两个不同的事物)
- 这两个图都并没有用到导航栏,而是将导航栏隐藏了,如果是在导航栏上面进行操作,会有很多不便
- 封装了继承自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认识