iOS组件封装与自动布局自定义表情键盘
下面的东西是编写自定义的表情键盘,话不多说,开门见山吧!下面主要用到的知识有mvc, ios开发中的自动布局,自定义组件的封装与使用,block回调,coredata的使用。有的小伙伴可能会问写一个自定义表情键盘肿么这么麻烦?下面 将会介绍我们如何用上面提到的东西来定义我们的表情键盘的。下面的内容会比较多,这篇文章还是比较有料的。
还是那句话写技术博客是少不了代码的,下面会结合代码来回顾一下ios的知识,本篇博文中用到的知识点在前面的博客中都能找到相应的内容,本篇 算是一个小小的功能整合。先来张图看一下本app的目录结构。我是根据自己对mvc的理解来构建的目录结构,希望起到抛砖引玉的作用,有好的解决方案欢迎 评论或者留言指出。face文件中存放的时我们的表情图片,model文件封装的是从sqlite中读取历史头像的组件,view文件中封装的时我们自定义的组件,也就是自定义键盘相关的视图,controller负责将我们的各个组件组装到一起完成我们想要的功能。下面会一一介绍。
上面是文件的组织结构,下面为了更为直观的了解我们想要的效果,下面先看几张截图,来直观的感受一下运行效果,上面是竖屏的显示效果,下面是横 屏的显示效果。因为在封装自定义键盘中用到了自动布局所以横屏显示或者在更大的屏幕上显示是没问题的,常用表情是用户用过的表情,然后存在sqlite 中,显示时并按时间降序排列。more是用来扩展功能用的接口。话不多说,来的代码才是实在的。
一.view(自定义视图)
view文件夹下存放的时我们自定义的视图组件,因为是自定义的组件所以storyboard我们就用不了啦,所有的代码都必须手写,这样 才能保证组件使用的灵活性和减少各个组件之间的耦合性,更利于团队之间的合作。在封装组件时要预留好外界可能使用到的接口,和返回该返回的数据。好啦,废话少说,来点干货吧!
1、faceview组件的封装:faceview即负责显示一个个的头像。在使用该组件时要传入要显示的图片和图片对应的文字(如【哈 哈】),当点击图片的时候,会通过block回调的形式把该图片的image以及图片文字返回到使用的组件中去,下面是关键代码:
faceview.h中的代码如下(下面代码是定义啦相应的block类型和对外的接口):
#import //声明表情对应的block,用于把点击的表情的图片和图片信息传到上层视图 typedef void (^faceblock) (uiimage *image, nsstring *imagetext); @interface faceview : uiview //图片对应的文字 @property (nonatomic, strong) nsstring *imagetext; //表情图片 @property (nonatomic, strong) uiimage *headerimage; //设置block回调 -(void)setfaceblock:(faceblock)block; //设置图片,文字 -(void)setimage:(uiimage *) image imagetext:(nsstring *) text; @end
faceview.m中的代码如下
// faceview.m // mykeyboard // // created by 青玉伏案 on 14-9-16. // copyright (c) 2014年 mrli. all rights reserved. // #import "faceview.h" @interface faceview () @property(strong, nonatomic) faceblock block; @property (strong, nonatomic) uiimageview *imageview; @end @implementation faceview //初始化图片 - (id)initwithframe:(cgrect)frame { //face的大小 frame.size.height = 30; frame.size.width = 30; self = [super initwithframe:frame]; if (self) { self.imageview = [[uiimageview alloc] initwithframe:cgrectmake(0, 0, 30, 30)]; [self addsubview:self.imageview]; } return self; } -(void) setfaceblock:(faceblock)block { self.block = block; } -(void) setimage:(uiimage *)image imagetext:(nsstring *)text { //显示图片 [self.imageview setimage:image]; //把图片存储起来 self.headerimage = image; self.imagetext = text; } //点击时回调 -(void)touchesended:(nsset *)touches withevent:(uievent *)event { uitouch *touch = [touches anyobject]; cgpoint point = [touch locationinview:self]; //判断触摸的结束点是否在图片中 if (cgrectcontainspoint(self.bounds, point)) { //回调,把该头像的信息传到相应的controller中 self.block(self.headerimage, self.imagetext); } } @end
代码说明:
主要就是block回调的使用,就是封装了一个自定义的button
2、functionview组件的封装,functionview就是使用faceview组件和scrollview组件把表情加载进来,在实例化functionview组件时,我们用到了自动布局来设置scrollview和下面的button.
functionview.h的代码如下,在.h中留有组件的接口和回调用的block, plistfilename用于加载我们的资源文件时使用。
// functionview.h // mykeyboard // // created by 青玉伏案 on 14-9-16. // copyright (c) 2014年 mrli. all rights reserved. // #import //定义对应的block类型,用于数据的交互 typedef void (^functionblock) (uiimage *image, nsstring *imagetext); @interface functionview : uiview //资源文件名 @property (nonatomic, strong) nsstring *plistfilename; //接受block块 -(void)setfunctionblock:(functionblock) block; @end
functionview.m中的代码如下,常用表情是在sqlite中获取的,而全部表情是通过plist文件的信息在face文件中加载的:
// functionview.m // mykeyboard // // created by 青玉伏案 on 14-9-16. // copyright (c) 2014年 mrli. all rights reserved. // #import "functionview.h" #import "faceview.h" #import "imagemodelclass.h" #import "historyimage.h" @interface functionview() @property (strong, nonatomic) functionblock block; //暂存表情组件回调的表情和表情文字 @property (strong, nonatomic) uiimage *headerimage; @property (strong, nonatomic) nsstring *imagetext; //display我们的表情图片 @property (strong, nonatomic) uiscrollview *headerscrollview; //定义数据模型用于获取历史表情 @property (strong, nonatomic) imagemodelclass *imagemodel; @end @implementation functionview - (id)initwithframe:(cgrect)frame { self = [super initwithframe:frame]; if (self) { //实例化数据模型 self.imagemodel =[[imagemodelclass alloc] init]; //实例化下面的button uibutton *facebutton = [[uibutton alloc] initwithframe:cgrectzero]; facebutton.backgroundcolor = [uicolor graycolor]; [facebutton settitle:@"全部表情" forstate:uicontrolstatenormal]; [facebutton setshowstouchwhenhighlighted:yes]; [facebutton addtarget:self action:@selector(tapbutton1:) forcontrolevents:uicontroleventtouchupinside]; [self addsubview:facebutton]; //实例化常用表情按钮 uibutton *morebutton = [[uibutton alloc] initwithframe:cgrectzero]; morebutton.backgroundcolor = [uicolor orangecolor]; [morebutton settitle:@"常用表情" forstate:uicontrolstatenormal]; [morebutton setshowstouchwhenhighlighted:yes]; [morebutton addtarget:self action:@selector(tapbutton2:) forcontrolevents:uicontroleventtouchupinside]; [self addsubview:morebutton]; //给按钮添加约束 facebutton.translatesautoresizingmaskintoconstraints = no; morebutton.translatesautoresizingmaskintoconstraints = no; //水平约束 nsarray *buttonh = [nslayoutconstraint constraintswithvisualformat:@"h:|[facebutton][morebutton(==facebutton)]|" options:0 metrics:0 views:nsdictionaryofvariablebindings(facebutton,morebutton)]; [self addconstraints:buttonh]; //垂直约束 nsarray *button1v = [nslayoutconstraint constraintswithvisualformat:@"v:[facebutton(44)]|" options:0 metrics:0 views:nsdictionaryofvariablebindings(facebutton)]; [self addconstraints:button1v]; nsarray *button2v = [nslayoutconstraint constraintswithvisualformat:@"v:[morebutton(44)]|" options:0 metrics:0 views:nsdictionaryofvariablebindings(morebutton)]; [self addconstraints:button2v]; //默认显示表情图片 [self tapbutton1:nil]; } return self; } //接受回调 -(void)setfunctionblock:(functionblock)block { self.block = block; } //点击全部表情按钮回调方法 -(void)tapbutton1: (id) sender { // 从plist文件载入资源 nsbundle *bundle = [nsbundle mainbundle]; nsstring *path = [bundle pathforresource:self.plistfilename oftype:@"plist"]; nsarray *headers = [nsarray arraywithcontentsoffile:path]; if (headers.count == 0) { nslog(@"访问的plist文件不存在"); } else { //调用headers方法显示表情 [self header:headers]; } } //点击历史表情的回调方法 -(void) tapbutton2: (id) sender { //从数据库中查询所有的图片 nsarray *imagedata = [self.imagemodel queryall]; //解析请求到的数据 nsmutablearray *headers = [nsmutablearray arraywithcapacity:imagedata.count]; //数据实体,相当于javabean的东西 historyimage *tempdata; for (int i = 0; i < imagedata.count; i ++) { tempdata = imagedata[i]; //解析数据,转换成函数headers要用的数据格式 nsmutabledictionary *dic = [nsmutabledictionary dictionarywithcapacity:2]; [dic setobject:tempdata.imagetext forkey:@"chs"]; uiimage *image = [uiimage imagewithdata:tempdata.headerimage]; [dic setobject:image forkey:@"png"]; [headers addobject:dic]; } [self header:headers]; } //负责把查出来的图片显示 -(void) header:(nsarray *)headers { [self.headerscrollview removefromsuperview]; self.headerscrollview = [[uiscrollview alloc] initwithframe:cgrectzero]; [self addsubview:self.headerscrollview]; //给scrollview添加约束 self.headerscrollview.translatesautoresizingmaskintoconstraints = no; //水平约束 nsarray *scrollh = [nslayoutconstraint constraintswithvisualformat:@"h:|-10-[_headerscrollview]-10-|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_headerscrollview)]; [self addconstraints:scrollh]; //垂直约束 nsarray *scrolv = [nslayoutconstraint constraintswithvisualformat:@"v:|-10-[_headerscrollview]-50-|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_headerscrollview)]; [self addconstraints:scrolv]; cgfloat scrollheight = (self.frame).size.height-60; //根据图片量来计算scrollview的contain的宽度 cgfloat width = (headers.count/(scrollheight/30))*30; self.headerscrollview.contentsize = cgsizemake(width, scrollheight); self.headerscrollview.pagingenabled = yes; //图片坐标 cgfloat x = 0; cgfloat y = 0; //往scroll上贴图片 for (int i = 0; i < headers.count; i ++) { //获取图片信息 uiimage *image; if ([headers[i][@"png"] iskindofclass:[nsstring class]]) { image = [uiimage imagenamed:headers[i][@"png"]]; } else { image = headers[i][@"png"]; } nsstring *imagetext = headers[i][@"chs"]; //计算图片位置 y = (i%(int)(scrollheight/30)) * 30; x = (i/(int)(scrollheight/30)) * 30; faceview *face = [[faceview alloc] initwithframe:cgrectmake(x, y, 0, 0)]; [face setimage:image imagetext:imagetext]; //face的回调,当face点击时获取face的图片 __weak __block functionview *copy_self = self; [face setfaceblock:^(uiimage *image, nsstring *imagetext) { copy_self.block(image, imagetext); }]; [self.headerscrollview addsubview:face]; } [self.headerscrollview setneedsdisplay]; } @end
代码说明:
1、主要是通过对资源文件或者对从数据库中查询的资源进行遍历然后添加到scrollview中
2.为了适应不同的屏幕给相应的组件添加了约束
3.toolview组件的封装: toolview就是在主屏幕上下面的类似于tabbar的东西,当键盘出来的时候,toolview会运动到键盘上面的位置。为了使用不同的屏幕,也需要用自动布局来实现。
toolview.h的代码如下:预留组件接口和声明block类型
// toolview.h // mykeyboard // // created by 青玉伏案 on 14-9-16. // copyright (c) 2014年 mrli. all rights reserved. // /***************** 封装下面的工具条组件 *****************/ #import //定义block块变量类型,用于回调,把本view上的按钮的index传到controller中 typedef void (^toolindex) (nsinteger index); @interface toolview : uiview //块变量类型的setter方法 -(void)settoolindex:(toolindex) toolblock; @end toolview.m的代码实现: // toolview.m // mykeyboard // // created by 青玉伏案 on 14-9-16. // copyright (c) 2014年 mrli. all rights reserved. // #import "toolview.h" @interface toolview () //定义toolindex类型的block,用于接受外界传过来的block @property (nonatomic, strong) toolindex myblock; @end @implementation toolview - (id)initwithframe:(cgrect)frame { self = [super initwithframe:frame]; if (self) { //1初始化表情按钮 uibutton *facebutton = [[uibutton alloc] initwithframe:cgrectzero]; facebutton.backgroundcolor = [uicolor orangecolor]; [facebutton settitle:@"表情" forstate:uicontrolstatenormal]; [facebutton setshowstouchwhenhighlighted:yes]; [facebutton addtarget:self action:@selector(tapfacebutton:) forcontrolevents:uicontroleventtouchupinside]; [self addsubview:facebutton]; //初始化更多按钮 uibutton *morebutton = [[uibutton alloc] initwithframe:cgrectzero]; morebutton.backgroundcolor = [uicolor graycolor]; [morebutton settitle:@"more" forstate:uicontrolstatenormal]; [morebutton setshowstouchwhenhighlighted:yes]; [morebutton addtarget:self action:@selector(tapmorebutton:) forcontrolevents:uicontroleventtouchupinside]; [self addsubview:morebutton]; //给我们的按钮添加约束来让按钮来占满toolview; facebutton.translatesautoresizingmaskintoconstraints = no; morebutton.translatesautoresizingmaskintoconstraints = no; //添加水平约束 nsarray *buttonh = [nslayoutconstraint constraintswithvisualformat:@"h:|[facebutton][morebutton(==facebutton)]|" options:0 metrics:0 views:nsdictionaryofvariablebindings(facebutton,morebutton)]; [self addconstraints:buttonh]; //添加垂直约束 nsarray *button1v = [nslayoutconstraint constraintswithvisualformat:@"v:|[facebutton]|" options:0 metrics:0 views:nsdictionaryofvariablebindings(facebutton)]; [self addconstraints:button1v]; nsarray *button2v = [nslayoutconstraint constraintswithvisualformat:@"v:|[morebutton]|" options:0 metrics:0 views:nsdictionaryofvariablebindings(morebutton)]; [self addconstraints:button2v]; } return self; } //接受传入的回调 -(void) settoolindex:(toolindex)toolblock { self.myblock = toolblock; } //点击表情按钮要回调的方法 -(void) tapfacebutton: (id) sender { self.myblock(1); } //点击more要回调的方法 -(void) tapmorebutton: (id) sender { self.myblock(2); } @end
代码说明:
主要是对block回调的应用和给相应的组件添加相应的约束。
4.moreview组件的封装代码就不往上贴啦,和上面的类似,下面是调用moreview组件的运行效果,有兴趣的读者请自行编写,以上就是视图部分的代码了
二. mode部分的内容
1.先定义我们要使用的数据模型,数据模型如下,time是使用表情的时间,用于排序。
2.下面编写我们的imagemodelclass类,里面封装了我们操作数据要用的方法
imagemodelclass.h的代码如下,主要是预留的对外的接口:
// // imagemodelclass.h // mykeyboard // // created by 青玉伏案 on 14-9-16. // copyright (c) 2014年 mrli. all rights reserved. // #import #import #import "historyimage.h" @interface imagemodelclass : nsobject //保存数据 -(void)save:(nsdata *) image imagetext:(nsstring *) imagetext; //查询所有的图片 -(nsarray *) queryall; @end
imagemodelclass.m的代码如下,主要是用coredata对sqlite的操作:
// imagemodelclass.m // mykeyboard // // created by 青玉伏案 on 14-9-16. // copyright (c) 2014年 mrli. all rights reserved. // #import "imagemodelclass.h" @interface imagemodelclass () @property (nonatomic, strong) nsmanagedobjectcontext *manager; @end @implementation imagemodelclass - (instancetype)init { self = [super init]; if (self) { //通过上下文获取manager uiapplication *application = [uiapplication sharedapplication]; id delegate = application.delegate; self.manager = [delegate managedobjectcontext]; } return self; } -(void)save:(nsdata *)image imagetext:(nsstring *)imagetext { if (image != nil) { nsarray *result = [self search:imagetext]; historyimage *myimage; if (result.count == 0) { myimage = [nsentitydescription insertnewobjectforentityforname:nsstringfromclass([historyimage class]) inmanagedobjectcontext:self.manager]; myimage.imagetext = imagetext; myimage.headerimage = image; myimage.time = [nsdate date]; } else { myimage = result[0]; myimage.time = [nsdate date]; } //存储实体 nserror *error = nil; if (![self.manager save:&error]) { nslog(@"保存出错%@", [error localizeddescription]); } } } //查找 -(nsarray *)search:(nsstring *) image { nsarray *result; //新建查询条件 nsfetchrequest *fetchrequest = [[nsfetchrequest alloc] initwithentityname:nsstringfromclass([historyimage class])]; //添加谓词 nspredicate *predicate = [nspredicate predicatewithformat:@"imagetext=%@",image]; //把谓词给request [fetchrequest setpredicate:predicate]; //执行查询 nserror *error = nil; result = [self.manager executefetchrequest:fetchrequest error:&error]; if (error) { nslog(@"查询错误:%@", [error localizeddescription]); } return result; } //查询所有的 -(nsarray *) queryall { //新建查询条件 nsfetchrequest *fetchrequest = [[nsfetchrequest alloc] initwithentityname:nsstringfromclass([historyimage class])]; //添加排序规则 //定义排序规则 nssortdescriptor *sortdescriptor = [[nssortdescriptor alloc] initwithkey:@"time" ascending:no]; //添加排序规则 [fetchrequest setsortdescriptors:@[sortdescriptor]]; //执行查询 nserror *error = nil; nsarray *result = [self.manager executefetchrequest:fetchrequest error:&error]; if (error) { nslog(@"查询错误:%@", [error localizeddescription]); } return result; } @end
代码说明:
1.保存图片时先查找图片是否存在,如果存在则更新时间,如果不存在则插入数据(写到这感觉想在用hibernate写东西)。
三.controller部分,把上面的组件进行组装
1.mainviewcontroller.m中的延展部分的代码如下:
@interface mainviewcontroller () //自定义组件 @property (nonatomic, strong) toolview *toolview; @property (nonatomic, strong) functionview *functionview; @property (nonatomic, strong) moreview *moreview; //系统组件 @property (strong, nonatomic) iboutlet uitextview *mytextview; @property (strong, nonatomic) nsdictionary *keyboarddic; @property (strong, nonatomic) iboutlet uiimageview *imageview; @property (strong, nonatomic) nsstring *sendstring; //数据model @property (strong, nonatomic) imagemodelclass *imagemode; @property (strong, nonatomic)historyimage *tempimage; @end
2.在viewdidload中进行组件的初始化和实现组件的block回调,代码如下
- (void)viewdidload { [super viewdidload]; //从sqlite中读取数据 self.imagemode = [[imagemodelclass alloc] init]; //实例化functionview self.functionview = [[functionview alloc] initwithframe:cgrectmake(0, 0, 320, 216)]; self.functionview.backgroundcolor = [uicolor blackcolor]; //设置资源加载的文件名 self.functionview.plistfilename = @"emoticons"; __weak __block mainviewcontroller *copy_self = self; //获取图片并显示 [self.functionview setfunctionblock:^(uiimage *image, nsstring *imagetext) { nsstring *str = [nsstring stringwithformat:@"%@%@",copy_self.mytextview.text, imagetext]; copy_self.mytextview.text = str; copy_self.imageview.image = image; //把使用过的图片存入sqlite nsdata *imagedata = uiimagepngrepresentation(image); [copy_self.imagemode save:imagedata imagetext:imagetext]; }]; //实例化moreview self.moreview = [[moreview alloc] initwithframe:cgrectmake(0, 0, 0, 0)]; self.moreview.backgroundcolor = [uicolor blackcolor]; [self.moreview setmoreblock:^(nsinteger index) { nslog(@"moreindex = %d",index); }]; //进行toolview的实例化 self.toolview = [[toolview alloc] initwithframe:cgrectzero]; self.toolview.backgroundcolor = [uicolor blackcolor]; [self.view addsubview:self.toolview]; //给toolview添加约束 //开启自动布局 self.toolview.translatesautoresizingmaskintoconstraints = no; //水平约束 nsarray *toolhconstraint = [nslayoutconstraint constraintswithvisualformat:@"h:|[_toolview]|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_toolview)]; [self.view addconstraints:toolhconstraint]; //垂直约束 nsarray *toolvconstraint = [nslayoutconstraint constraintswithvisualformat:@"v:[_toolview(44)]|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_toolview)]; [self.view addconstraints:toolvconstraint]; //回调toolview中的方法 [self.toolview settoolindex:^(nsinteger index) { nslog(@"%d", index); switch (index) { case 1: [copy_self changekeyboardtofunction]; break; case 2: [copy_self changekeyboardtomore]; break; default: break; } }]; //当键盘出来的时候通过通知来获取键盘的信息 //注册为键盘的监听着 nsnotificationcenter *center = [nsnotificationcenter defaultcenter]; [center addobserver:self selector:@selector(keynotification:) name:uikeyboardwillchangeframenotification object:nil]; //给键盘添加dan //textview的键盘定制回收按钮 uitoolbar * toolbar = [[uitoolbar alloc]initwithframe:cgrectmake(0, 0, 320, 30)]; uibarbuttonitem * item1 = [[uibarbuttonitem alloc]initwithbarbuttonsystemitem:uibarbuttonsystemitemdone target:self action:@selector(tapdone:)]; uibarbuttonitem * item2 = [[uibarbuttonitem alloc]initwithbarbuttonsystemitem:uibarbuttonsystemitemflexiblespace target:nil action:nil]; uibarbuttonitem * item3 = [[uibarbuttonitem alloc]initwithbarbuttonsystemitem:uibarbuttonsystemitemflexiblespace target:nil action:nil]; toolbar.items = @[item2,item1,item3]; self.mytextview.inputaccessoryview =toolbar; }
3.当横竖屏幕切换时设置自定义键盘的高度
-(void)willanimaterotationtointerfaceorientation:(uiinterfaceorientation)tointerfaceorientation duration:(nstimeinterval)duration { //纵屏 if (uiinterfaceorientationisportrait(tointerfaceorientation)) { cgrect frame = self.functionview.frame; frame.size.height = 216; self.functionview.frame = frame; self.moreview.frame = frame; } //横屏 if (uiinterfaceorientationislandscape(tointerfaceorientation)) { cgrect frame = self.functionview.frame; frame.size.height = 150; self.functionview.frame = frame; self.moreview.frame = frame; } }
4.当键盘出来的时候,改变toolview的位置,通过键盘的通知来实现。当横屏的时候键盘的坐标系和我们当前的frame的坐标系不一样所以当横屏时得做一坐标系的转换,代码如下:
//当键盘出来的时候改变toolview的位置(接到键盘出来的通知要做的方法) -(void) keynotification : (nsnotification *) notification { nslog(@"%@", notification.userinfo); self.keyboarddic = notification.userinfo; //获取键盘移动后的坐标点的坐标点 cgrect rect = [self.keyboarddic[@"uikeyboardframeenduserinfokey"] cgrectvalue]; //把键盘的坐标系改成当前我们window的坐标系 cgrect r1 = [self.view convertrect:rect fromview:self.view.window]; [uiview animatewithduration:[self.keyboarddic[uikeyboardanimationdurationuserinfokey] floatvalue] animations:^{ //动画曲线 [uiview setanimationcurve:[self.keyboarddic[uikeyboardanimationcurveuserinfokey] doublevalue]]; cgrect frame = self.toolview.frame; frame.origin.y = r1.origin.y - frame.size.height; //根据键盘的高度来改变toolview的高度 self.toolview.frame = frame; }]; }
5.系统键盘和自定义键盘切换的代码如下:
//切换键盘的方法 -(void) changekeyboardtofunction { if ([self.mytextview.inputview isequal:self.functionview]) { self.mytextview.inputview = nil; [self.mytextview reloadinputviews]; } else { self.mytextview.inputview = self.functionview; [self.mytextview reloadinputviews]; } if (![self.mytextview isfirstresponder]) { [self.mytextview becomefirstresponder]; } }
以上就是上面展示效果的核心代码了,在做的时候感觉难点在于如何进行屏幕适配,尤其是当屏幕横过来的时候键盘的坐标系和我们frame的坐标系不同,得做 一个转换。发表文章的目的是想起到抛砖引玉的左右,有好的东西希望大家相互交流一下。
推荐阅读