您的位置:首页 > 产品设计 > UI/UE

iOS开发-------自定义简单的表情键盘(UICollectionView 集合视图)

2015-10-04 15:24 501 查看
最近制作自制表情键盘的时候,突然了解到还有一个叫做UICollectionView (集合视图)的类,就研究了一下,确实在做表情键盘上要比用 UIScrollView(滚动视图) 要简单的多,用法与 UITableView(表格视图) 相似,但楼主觉得稍微麻烦那么一点点,就是因为它必须要自顶一个cell,这个cell的类型是UICollectionViewCell,用到的仅仅是初级的用法,高级用法也请查看其他资料。

这一次按照自己的理解,用了一下自认为的MVC架构模式,如理解不妥,特请指正,话不多说,首先看一我的工程目录吧



接着看一下运行的结果吧,因为加了约束,所以横屏竖屏都是可以的





其实一般情况下,打印的表情发送到服务器的形式就是文本中的形式,以[ ]作为标识符,如果想在文本框中打出表情也是可以的,可以去看一下之前的一篇博客 iOS学习-------文字表情(NSAttributeString
属性字符串 以及 NSRegularExpression 正则表达类)

因为表情是通过本地的plist文件来读取的,所以首先看一下plist文件



整体是一个数组,每个数组元素都是一个字典,那么看到字典,首先想到的就是模型了

Model(模型)

首先创建一个表情类,即Emoticon类,存取数据的,以后的操作就是对表情(Emoticon)类的操作,而不是对字典的操作,首先是头文件
//
//  Emoticon.h
//  表情键盘
//
//  Created by YueWen on 15/10/3.
//  Copyright (c) 2015年 YueWen. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface Emoticon : NSObject

/**
*  繁体的汉字
*/
@property(nonatomic,strong)NSString * cht;

/**
*  动态图名
*/
@property(nonatomic,strong)NSString * gif;

/**
*  种类
*/
@property(nonatomic,strong)NSNumber * type;

/**
*  简体的汉字
*/
@property(nonatomic,strong)NSString * chs;

/**
*  静态图名
*/
@property(nonatomic,strong)NSString * png;

/**
*  自定义初始化方法
*
*  @param dict 含参字典
*
*  @return 返回创建好的Emoticon对象
*/
-(instancetype)initWithDictionary:(NSDictionary *)dict;

/**
*  便利构造器
*
*  @param dict 含参字典
*
*  @return 返回创建好的Emoticon对象
*/
+(instancetype)emoticonWithDictionary:(NSDictionary *)dict;

@end


模型类很简单,只需要在实现.m文件中,实现 便利初始化方法 以及 便利构造器 即可,如果对便利初始化方法以及便利构造器不熟悉,也欢迎去之前的博客Objective-C学习- 便利初始化函数和便利构造器去复习一下。实现方法如下

首先实现便利初始化方法
- (instancetype)initWithDictionary:(NSDictionary *)dict
{
self = [super init];
if (self)
{
//通过MVC进行赋值
[self setValuesForKeysWithDictionary:dict];
}
return self;
}


目前只见过3种 '+' 方法,便利构造器,单例以及纯过程封装,下面便是便利构造器

+(instancetype)emoticonWithDictionary:(NSDictionary *)dict
{

__autoreleasing Emoticon * emoticon = [[Emoticon alloc]initWithDictionary:dict];

return emoticon;
}


因为不是很过复杂,所以没有写Manager(管理者),一般情况下,Manager是一个单例。

View (视图)

自定义的UICollectionViewCell

因为在UICollectionView(集合视图)中,没有默认的Cell,这点不像是UITableViewCell,因为没有用storyBoard拖东西,所以用的纯代码进行的布局,首先创建了一个叫做BaseCollectionViewCell (Cell 基类) ,为了以后好拓展,虽然看起来在做这个的时候会麻烦点。

创建的基类(BaseCollectionViewCell)如下
#import <UIKit/UIKit.h>
#import "Emoticon.h"

typedef void(^emoticonButtonClickBlock)(UIImage * buttonImage,NSString * imageName);

@interface BaseCollectionViewCell : UICollectionViewCell

/**
*  根据表情模型赋值
*
*  @param emoticon 表情模型
*/
-(void)setInformationWithEmoticon:(Emoticon *)emoticon;

/**
*  设置block回调
*
*  @param emoticonButtonBlock 回调的代码块
*/
-(void)emotionButtonClickBlockHandle:(emoticonButtonClickBlock)emoticonButtonBlock;

@end


实现方法木有,因为实现几乎没有意义

接着就是自定义的表情cell了,思路就是在cell上放置一个 button ,让 button 的 frame 大小等于 cell 的bounds,当点击的时候,通过回调返回点击按钮的表情以及文字标识符。但是返回的属性UIImage 和 imageName 是不能被修改的,所以属性上用的 readOnly,头文件如下
#import <UIKit/UIKit.h>

typedef void(^emoticonButtonClickBlock)(UIImage * buttonImage,NSString * imageName);

@interface EmoticonCell : UICollectionViewCell

/**
*  button上的表情
*/
@property(nonatomic,strong,readonly)UIImage * buttonImage;

/**
*  button上表情的名字
*/
@property(nonatomic,strong,readonly)NSString * imageName;

@end


因为继承了BaseCollectionViewCell,所以在基类中没有实现的方法需要在EmoticonCell类中进行实现,首先实现基类中的两个方法
/**
*  设置block回调
*
*  @param emoticonButtonBlock 回调的代码块
*/
-(void)emotionButtonClickBlockHandle:(emoticonButtonClickBlock)emoticonButtonBlock
{
self.b = emoticonButtonBlock;
}

/**
*  根据表情模型赋值
*
*  @param emoticon 表情模型
*/
-(void)setInformationWithEmoticon:(Emoticon *)emoticon
{
UIImage * image = [UIImage imageNamed:emoticon.png];

[self setButtonImage:image WithImageName:emoticon.chs];

}


在setInfomationWithEmoticon:方法中用到了一个自定义的设置的方法,是之前写的,又不想删除,所以就起到了一个简化代码的作用,很简单的实现,如下
/**
*  设置button上按钮的图片和图片名字
*
*  @param buttonImage 按钮上的图片
*  @param imageName   按钮上图片的名字
*/
-(void)setButtonImage:(UIImage *)buttonImage WithImageName:(NSString *)imageName
{
_buttonImage = buttonImage;
_imageName = imageName;

//设置button属性
[self.emoticonButton setImage:_buttonImage forState:UIControlStateNormal];
}


因为Cell的基类说到底就是UIView,所以必须要重写两个创建方法,如下
//用纯代码创建的时候走的创建方法
-(instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
[self cellInit];
}

return self;
}

//用xib或者storyboard创建的时候走的创建方法
-(void)awakeFromNib
{
[self cellInit];
}


因为cellInit是自定义的创建方式,自然每个创建方法都要用,所以楼主打包成一个方法,偷个懒0.0
-(void)cellInit
{
//初始化按钮
self.emoticonButton = [UIButton buttonWithType:UIButtonTypeCustom];

//设置大小
self.emoticonButton.frame = self.bounds;

//注册回调方法
[self.emoticonButton addTarget:self action:@selector(emoticonButtonClick) forControlEvents:UIControlEventTouchUpInside];

//添加button
[self addSubview:self.emoticonButton];

}


当button被点击的时候,之前的回调就有用了
/**
*  按钮被点击的时候,对父控件进行回调
*/
-(void)emoticonButtonClick
{
//如果自己的回调被赋值后,才进行回调
if (self.b)
{
self.b(_buttonImage,_imageName);
}
}


这样自定义的Cell类就完成了。

EmoticonsView(展示表情的视图)

EmoticonsView是在这里面最复杂的一个视图了,与其说它复杂不是因为逻辑复杂,也不是因为代码多,而是这里面用到了一个以前没有接触过的UICollectionView(集合视图)类,之前的一切也都是为了它再做铺垫,但是好处就是他的用法与UITableView(表格视图很像),这也提供了一些参考,话不多说,希望代码的写法能给大家一些启发,自然这里的用法都是最基础的用法,并不能代表这个组件已经完全驾驭。
首先因为他不是最后的boss(ViewController),所以也是需要汇报的,自然是少不了回调的,因为用Block回调比较多,所以依旧选择用Block回调,下面是声明文件.h中的声明:
#import <UIKit/UIKit.h>

typedef void(^emoticonsViewWithButtonPressedBlock)(UIImage * image, NSString * imageName);

@interface EmoticonsView : UIView

/**
*  设置block回调
*
*  @param buttonPressedBlock 返回代码块
*/
-(void)emoticonsViewWithButtonPressedBlockHandle:(emoticonsViewWithButtonPressedBlock)buttonPressedBlock;

@end


接着就是在延展中,加入三个属性对象,分别用来显示表情(collectionView),存储表情的信息(emoticons),以及回调的方法Block(b),但是有一点不同,要用UICollectionView,要遵守三个协议,不是UITableView的两个协议,因为多了一个类似布局的协议,如下
@interface EmoticonsView ()<UICollectionViewDataSource,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout>

//视图集合
@property(nonatomic,strong)UICollectionView * collectionView;

//存储表情的数组
@property(nonatomic,strong)NSArray * emoticons;

//回调代码块
@property(nonatomic,strong)emoticonsViewWithButtonPressedBlock b;

@end


因为是view,所以重写的那两个方法不再赘余,只说明自写的init方法
-(void)viewInit
{
//配置collectionView
[self loadCollectionView];

//注册cell
[self registerClassWithCell];

//加载数组
self.emoticons = [self loadEmoticons];

//贴到视图上
[self addSubview:self.collectionView];

//开始适配
[self layoutCollectionView];
}


先从最简单的开始,加载数组,思路是先从plist文件中加载数据,然后通过遍历出的字典,通过字典转模型,再返回处理好的数组,如下
#pragma mark - 表情数组的加载
/**
*  加载表情数据
*
*  @return 返回存储表情的数组
*/
-(NSArray *)loadEmoticons
{
//路径
NSString * path = [[NSBundle mainBundle]pathForResource:@"emoticons" ofType:@"plist"];

//加载数据
NSArray * emoticons = [NSArray arrayWithContentsOfFile:path];

//整合的数组
NSMutableArray * mutableEmoticons = [NSMutableArray array];

//可变数组进行数据整合
for (NSDictionary * infoDict in emoticons)
{
//字典转模型
Emoticon * emoticon = [Emoticon emoticonWithDictionary:infoDict];

//可变数组添加
[mutableEmoticons addObject:emoticon];
}

return [NSArray arrayWithArray:mutableEmoticons];
}


因为没有storyboard直接用UICollectionViewController,所以要结合自定义cell,只能通过在init中进行注册,为什么要单独出一个方法呢,不就是一句话么,如果cell足够多的话,只需要改动这个方法即可,不需要改动init方法。
/**
*  注册各种cell
*/
-(void)registerClassWithCell
{
//注册cell,让cell的重用标识符是@“Emoticon”
[self.collectionView registerClass:[EmoticonCell class] forCellWithReuseIdentifier:@"Emoticon"];
}


倒数第二麻烦的就应该是布局了,虽然代码看起来很爽,但是确实是说,用stroyboard来布局是非常高效的,但楼主用的是代码。需要注意的就是一点,必须先添加到父视图上才可以进行布局,不然会出很多麻烦
-(void)layoutCollectionView
{
//水平适配
NSArray * horizontal = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_collectionView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_collectionView)];
[self addConstraints:horizontal];

//垂直适配
NSArray * verital = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_collectionView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_collectionView)];
[self addConstraints:verital];
}


最麻烦的就是数UICollectionView了,因为它有着各种协议以及各种配置问题,因为有注释,所以很明确,看一下配置方法
/**
*  配置集合视图(CollectionView)
*/
-(void)loadCollectionView
{
//集合视图的布局对象,必须有!如果不想设置其他的属性,就只有这句init即可
UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc]init];

//水平滚动,也就说布局是竖直优先
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;

//初始化集合视图(UICollenctionView)
self.collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:layout];

//手动适配屏幕
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;

//分页显示,否则太滑,继承于ScrollView
[self.collectionView setPagingEnabled:YES];

//默认是黑色的
self.collectionView.backgroundColor = [UIColor groupTableViewBackgroundColor];

//不显示水平滚动栏
self.collectionView.showsHorizontalScrollIndicator = NO;

//设置代理和数据源
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
}


首先实现集合视图的数据源方法,首先看两个,很好理解,和UITableView简直就是双胞胎,只不过tableView用row,而CollectionView用item
#pragma mark - UICollectionView DateSource
//返回几个组,也是分组的,默认也是1,这个和UITableView一致
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}

//返回组中的数据的个数
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.emoticons.count;
}


接着就是实现自定义cell的方法,与UItableView稍有不同,但也是很好理解的
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//获取数据
Emoticon * emoticon = self.emoticons[indexPath.row];

//创建cell
BaseCollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([emoticon class]) forIndexPath:indexPath];

//避免强引用循环
__weak __block EmoticonsView * copy_self = self;

//对cell设置进行的回调
[cell emotionButtonClickBlockHandle:^(UIImage *buttonImage, NSString *imageName) {

//如果上报代码块存在
if (copy_self.b)
{
copy_self.b(buttonImage,imageName);
}

}];

//设置数据
[cell setInformationWithEmoticon:emoticon];

return  cell;
}


因为UICollectionViewDelegate的方法没有用到的,所以没有写相关方法,直接用的UICollectionViewDelegateFlowLayout方法,更多方法也可以按住option点击进入开发文档进行查看,方法如下
#pragma mark - UICollectionView DelegateFlowLayout

//每个item的大小(可以根据indexPath定制)
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return CGSizeMake(30, 30);
}

//每组距离边界的大小,逆时针,上,左,下,右
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(20, 20, 20, 20);
}


显示的view完毕

MyTextField (文本框)

MyTextField 其实是继承于UITextView,但是楼主当时打的太快,所以起错了名字,改的时候又太麻烦,所以就不改了,这个类的作用就是能够调用我们之前写好的表情键盘的类,它需要知道目前键盘的类型是啥,也就是一个标志位。为了显得高大上一点,用的枚举
typedef enum : NSUInteger {
KeyBoardTypeSystem,
KeyBoardTypeFace,
} KeyBoardType;


声明文件中只需要声明一个可查看键盘状态的属性,一个切换键盘的方法和具体改变键盘的方法即可
@interface MyTextField : UITextView

/**
*  当前键盘的状态
*/
@property(nonatomic,assign,readonly)KeyBoardType currentKeyBoardType;

/**
*  切换键盘的方法
*/
-(void)switchKeyBoard;

/**
*  改变键盘
*
*  @param keyBoard 改变键盘的样式
*/
-(void)changeKeyBoard:(KeyBoardType)keyBoard;

@end


在延展中只需要一个表情视图(EmoticonsView)即可
@interface MyTextField ()

@property(nonatomic,strong)EmoticonsView * emoticonView;

@end


自定义的init方法
-(void)fieldInit
{
//初始化键盘view
self.emoticonView = [[EmoticonsView alloc]initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 200)];

//初始化当前状态
_currentKeyBoardType = KeyBoardTypeSystem;

//避免强引用循环
__block __weak MyTextField * copy_self = self;

//设置回调
[self.emoticonView emoticonsViewWithButtonPressedBlockHandle:^(UIImage *image, NSString *imageName) {
//为text设置内容
copy_self.text = [copy_self.text stringByAppendingString:imageName];
}];

}


实现切换键盘的两个方法即可

切换键盘的方法如下
-(void)switchKeyBoard
{
switch (self.currentKeyBoardType) {

//如果是表情键盘,就换成系统键盘
case KeyBoardTypeFace:
[self changeKeyBoard:KeyBoardTypeSystem];
break;

//如果是系统键盘,就换成表情键盘
case KeyBoardTypeSystem:
[self changeKeyBoard:KeyBoardTypeFace];
break;

default:
break;
}
}


改变键盘的方法如下
-(void)changeKeyBoard:(KeyBoardType)keyBoardType
{
//判断是什么键盘
switch (keyBoardType) {

//如果是系统键盘
case KeyBoardTypeSystem:
self.inputView = nil;//nil表示系统默认自带的键盘
break;

//如果是表情键盘
case KeyBoardTypeFace:
self.inputView = self.emoticonView;
break;

default:
break;
}

_currentKeyBoardType = keyBoardType;

//如果是第一响应就要刷新一下
if (self.isFirstResponder)
{
[self reloadInputViews];
}
}


Controller(控制器)

controller算是最后的小老板了,因为之前的铺垫,所以他的逻辑就变得很简单了

首先在延展中声明一个切换键盘的 按钮(UIButton) 以及 自定义的文本域(MyTextField) 即可
//文本域
@property(nonatomic,strong)MyTextField * textField;

//切换状态的按钮
@property(nonatomic,strong)UIButton * stateButton;


viewDidLoad上,楼主依旧用的是方法进行
#pragma mark - 自带的加载方法
- (void)viewDidLoad {
[super viewDidLoad];

//设置背景色
[self.view setBackgroundColor:[UIColor whiteColor]];

//加载按钮
[self loadStateButton];

//适配切换按钮
[self layoutStateButton];

//加载textField
[self loadTextView];

//适配textField
[self layoutTextView];//注意位置不可随意,必须在button适配好之后才可以,因为它是以button为基准的

}


首先加载button,用storyboard是很简单,因为用的是代码,所以看起来比较复杂,不做过多的解释,相信都能看得懂
/**
*  加载键盘状态转换按钮
*/
-(void)loadStateButton
{
//初始化button对象
self.stateButton = [UIButton buttonWithType:UIButtonTypeSystem];

//设置button的title
[self.stateButton setTitle:@"切换输入法" forState:UIControlStateNormal];

//解除自动适配
self.stateButton.translatesAutoresizingMaskIntoConstraints = NO;

//添加目标动作回调
[self.stateButton addTarget:self action:@selector(stateButtonClick) forControlEvents:UIControlEventTouchUpInside];

//添加组件
[self.view addSubview:self.stateButton];
}


接着是button的适配
/**
*  对切换按钮进行适配
*/
-(void)layoutStateButton
{
//水平适配
NSArray * horizontal = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_stateButton]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_stateButton)];
[self.view addConstraints:horizontal];

//垂直适配
NSArray * verital = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-20-[_stateButton]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_stateButton)];
[self.view addConstraints:verital];
}


按钮配置完毕后,就需要适配文本域了,加载方法如下
/**
*  加载textField
*/
-(void)loadTextView
{
//创建文本对象
self.textField = [[MyTextField alloc]initWithFrame:CGRectZero];

//设置背景色
self.textField.backgroundColor = [UIColor groupTableViewBackgroundColor];

//解除自动适配
self.textField.translatesAutoresizingMaskIntoConstraints = NO;

//添加视图
[self.view addSubview:self.textField];

}


为文本域添加约束
/**
*  适配文本框
*/
-(void)layoutTextView
{
//水平适配
NSArray * horizontal = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[_textField]-10-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_textField)];
[self.view addConstraints:horizontal];

//垂直布局
NSArray * verital = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[_stateButton]-8-[_textField(150)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_textField,_stateButton)];
[self.view addConstraints:verital];
}


切换按钮的回调方法,因为之前的铺垫,依旧很简单
/**
*  切换按钮点击
*/
-(void)stateButtonClick
{
[self.textField switchKeyBoard];
}


基本的功能到此结束。

但是这样完善有一个不舒服的地方,就是输入完毕东西之后,键盘不能自动消失,但是重新加一个按钮又显得很low,所以楼主用的是一个轻击手势,怎么用呢,在延展中加入一个属性
//轻击手势
@property(nonatomic,strong)UITapGestureRecognizer * tapGestureRecognizer;


在viewDidLoad中加入这个方法
//配置手势
[self loadTapGestureRecognizer];


配制方法
/**
*  加载轻击手势
*/
-(void)loadTapGestureRecognizer
{
//初始化轻击手势
self.tapGestureRecognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction)];

//必须双击
self.tapGestureRecognizer.numberOfTapsRequired = 2;

//添加手势
[self.view addGestureRecognizer:self.tapGestureRecognizer];

}


轻击后的回调方法
/**
*  轻击手势的目标动作回调
*/
-(void)tapAction
{
//如果是第一响应
if ([self.textField isFirstResponder])
{
//撤销键盘,解除第一响应
[self.view endEditing:YES];
}
}


这样,双击后键盘就自动撤掉了。更多手势识别可以去看看之前的博客iOS学习-------手势识别,3Q
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: