欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS开发之微信聊天工具栏的封装

程序员文章站 2023-08-12 14:36:28
微信大家基本上都用过,今天要做的就是微信的聊天工具条。聊天工具条还是比较复杂的,其中包括发送表情,发送文字,发送图片,发送声音,拍照等等功能,下面给出发送录音,文字,表情的...

微信大家基本上都用过,今天要做的就是微信的聊天工具条。聊天工具条还是比较复杂的,其中包括发送表情,发送文字,发送图片,发送声音,拍照等等功能,下面给出发送录音,文字,表情的代码,其他的和这几样类似。还是那句话百字不如一图,先来几张效果图吧。

iOS开发之微信聊天工具栏的封装

在封装聊天工具条的的时候表情键盘是之前封装好的,所以拿过来就可以用的啦。因为不管是工具条还是表情键盘都是用约束来控件大小的,所以横屏也是没问题的,在大屏手机上也是没问题的。下面将会一步步讲解如何封装下面的聊天工具条。主要是对工具条的封装,表情键盘在这就不做讲解了。
一、toolview预留的接口
在封装toolview中主要用到block回调,读者可以根据自己的个人习惯来选择是block回调,还是委托回调或者是目标动作回调(笔者更喜欢block回调),下面的代码是toolview给调用者提供的接口

//
// toolview.h
// mecromessage
//
// created by (青玉伏案)on 14-9-22.
// copyright (c) 2014年 mrli. all rights reserved.
//

#import <uikit/uikit.h>


//定义block类型把toolview中textview中的文字传入到controller中
typedef void (^mytextblock) (nsstring *mytext);

//录音时的音量
typedef void (^audiovolumeblock) (cgfloat volume);

//录音存储地址
typedef void (^audiourlblock) (nsurl *audiourl);

//改变根据文字改变textview的高度
typedef void (^contentsizeblock)(cgsize contentsize);

//录音取消的回调
typedef void (^cancelrecordblock)(int flag);


@interface toolview : uiview<uitextviewdelegate,avaudiorecorderdelegate>


//设置mytextblock
-(void) setmytextblock:(mytextblock)block;

//设置声音回调
-(void) setaudiovolumeblock:(audiovolumeblock) block;

//设置录音地址回调
-(void) setaudiourlblock:(audiourlblock) block;

-(void)setcontentsizeblock:(contentsizeblock) block;

-(void)setcancelrecordblock:(cancelrecordblock)block;

-(void) changefunctionheight: (float) height;

@end


二、初始化toolview中所需的控件
1.为了更好的封装我们的组件,在.h中预留接口,在toolview.m的延展中添加我们要使用的组件(私有属性),延展代码如下:

@interface toolview()
//最左边发送语音的按钮
@property (nonatomic, strong) uibutton *voicechangebutton;

//发送语音的按钮
@property (nonatomic, strong) uibutton *sendvoicebutton;

//文本视图
@property (nonatomic, strong) uitextview *sendtextview;

//切换键盘
@property (nonatomic, strong) uibutton *changekeyboardbutton;

//more
@property (nonatomic, strong) uibutton *morebutton;

//键盘坐标系的转换
@property (nonatomic, assign) cgrect endkeyboardframe;


//表情键盘
@property (nonatomic, strong) functionview *functionview;

//more
@property (nonatomic, strong) moreview *moreview;

//数据model
@property (strong, nonatomic) imagemodelclass *imagemode;

@property (strong, nonatomic)historyimage *tempimage;


//传输文字的block回调
@property (strong, nonatomic) mytextblock textblock;

//contentsinz
@property (strong, nonatomic) contentsizeblock sizeblock;

//传输volome的block回调
@property (strong, nonatomic) audiovolumeblock volumeblock;

//传输录音地址
@property (strong, nonatomic) audiourlblock urlblock;

//录音取消
@property (strong, nonatomic) cancelrecordblock cancelblock;


//添加录音功能的属性
@property (strong, nonatomic) avaudiorecorder *audiorecorder;

@property (strong, nonatomic) nstimer *timer;
@property (strong, nonatomic) nsurl *audioplayurl;

@end

2.接受相应的block回调,把block传入toolview中,代码如下:  

-(void)setmytextblock:(mytextblock)block
{
 self.textblock = block;
}

-(void)setaudiovolumeblock:(audiovolumeblock)block
{
 self.volumeblock = block;
}

-(void)setaudiourlblock:(audiourlblock)block
{
 self.urlblock = block;
}

-(void)setcontentsizeblock:(contentsizeblock)block
{
 self.sizeblock = block;
}

-(void)setcancelrecordblock:(cancelrecordblock)block
{
 self.cancelblock = block;
}

3.控件的初始化,纯代码添加toolview中要用到的组件(分配内存,配置相应的属性),因为是自定义组件的封装,所以我们的storyboard就用不上啦,添加控件的代码如下:

//控件的初始化
-(void) addsubview
{
 self.voicechangebutton = [[uibutton alloc] initwithframe:cgrectzero];
 [self.voicechangebutton setimage:[uiimage imagenamed:@"chat_bottom_voice_press.png"] forstate:uicontrolstatenormal];
 [self.voicechangebutton addtarget:self action:@selector(tapvoicechangebutton:) forcontrolevents:uicontroleventtouchupinside];
 [self addsubview:self.voicechangebutton];
 
 self.sendvoicebutton = [[uibutton alloc] initwithframe:cgrectzero];
 [self.sendvoicebutton setbackgroundimage:[uiimage imagenamed:@"chat_bottom_textfield.png"] forstate:uicontrolstatenormal];
 [self.sendvoicebutton settitlecolor:[uicolor blackcolor] forstate:uicontrolstatenormal];
 [self.sendvoicebutton settitle:@"按住说话" forstate:uicontrolstatenormal];
 
 
 [self.sendvoicebutton addtarget:self action:@selector(tapsendvoicebutton:) forcontrolevents:uicontroleventtouchupinside];
 self.sendvoicebutton.hidden = yes;
 [self addsubview:self.sendvoicebutton];
 
 self.sendtextview = [[uitextview alloc] initwithframe:cgrectzero];
 self.sendtextview.delegate = self;
 [self addsubview:self.sendtextview];
 
 self.changekeyboardbutton = [[uibutton alloc] initwithframe:cgrectzero];
 [self.changekeyboardbutton setimage:[uiimage imagenamed:@"chat_bottom_smile_nor.png"] forstate:uicontrolstatenormal];
 [self.changekeyboardbutton addtarget:self action:@selector(tapchangekeyboardbutton:) forcontrolevents:uicontroleventtouchupinside];
 [self addsubview:self.changekeyboardbutton];
 
 self.morebutton = [[uibutton alloc] initwithframe:cgrectzero];
 [self.morebutton setimage:[uiimage imagenamed:@"chat_bottom_up_nor.png"] forstate:uicontrolstatenormal];
 [self.morebutton addtarget:self action:@selector(tapmorebutton:) forcontrolevents:uicontroleventtouchupinside];
 [self addsubview:self.morebutton];
 
 [self adddone];
 
 
 
 //实例化functionview
 self.functionview = [[functionview alloc] initwithframe:cgrectmake(0, 0, 320, 216)];
 self.functionview.backgroundcolor = [uicolor blackcolor];
 
 //设置资源加载的文件名
 self.functionview.plistfilename = @"emoticons";
 
 __weak __block toolview *copy_self = self;
 //获取图片并显示
 [self.functionview setfunctionblock:^(uiimage *image, nsstring *imagetext)
  {
   nsstring *str = [nsstring stringwithformat:@"%@%@",copy_self.sendtextview.text, imagetext];
   
   copy_self.sendtextview.text = str;
   
   //把使用过的图片存入sqlite
   nsdata *imagedata = uiimagepngrepresentation(image);
   [copy_self.imagemode save:imagedata imagetext:imagetext];
  }];
 
 
 //给sendtextview添加轻击手势
 uitapgesturerecognizer *tapgesture = [[uitapgesturerecognizer alloc] initwithtarget:self action:@selector(tapgesture:)];
 [self.sendtextview addgesturerecognizer:tapgesture];
 
 
 //给sendvoicebutton添加长按手势
 uilongpressgesturerecognizer *longpress = [[uilongpressgesturerecognizer alloc] initwithtarget:self action:@selector(sendvoicebuttonlongpress:)];
 //设置长按时间
 longpress.minimumpressduration = 0.2;
 [self.sendvoicebutton addgesturerecognizer:longpress];
 
 //实例化moreview
 self.moreview = [[moreview alloc] initwithframe:cgrectmake(0, 0, 0, 0)];
 self.moreview.backgroundcolor = [uicolor blackcolor];
 [self.moreview setmoreblock:^(nsinteger index) {
  nslog(@"moreindex = %d",(int)index);
 }];

 
}

4.给我们的控件添加相应的约束,为了适合不同的屏幕,所以自动布局是少不了的。当然啦给控件添加约束也必须是手写代码啦,添加约束的代码如下:

//给控件加约束
-(void)addconstraint
{
 //给voicebutton添加约束
 self.voicechangebutton.translatesautoresizingmaskintoconstraints = no;
 
 nsarray *voiceconstrainth = [nslayoutconstraint constraintswithvisualformat:@"h:|-5-[_voicechangebutton(30)]" options:0 metrics:0 views:nsdictionaryofvariablebindings(_voicechangebutton)];
 [self addconstraints:voiceconstrainth];
 
 nsarray *voiceconstraintv = [nslayoutconstraint constraintswithvisualformat:@"v:|-8-[_voicechangebutton(30)]" options:0 metrics:0 views:nsdictionaryofvariablebindings(_voicechangebutton)];
 [self addconstraints:voiceconstraintv];
 
 
 
 //给morebutton添加约束
 self.morebutton.translatesautoresizingmaskintoconstraints = no;
 
 nsarray *morebuttonh = [nslayoutconstraint constraintswithvisualformat:@"h:[_morebutton(30)]-5-|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_morebutton)];
 [self addconstraints:morebuttonh];
 
 nsarray *morebuttonv = [nslayoutconstraint constraintswithvisualformat:@"v:|-8-[_morebutton(30)]" options:0 metrics:0 views:nsdictionaryofvariablebindings(_morebutton)];
 [self addconstraints:morebuttonv];
 
 
 //给changekeyboardbutton添加约束
 self.changekeyboardbutton.translatesautoresizingmaskintoconstraints = no;
 
 nsarray *changekeyboardbuttonh = [nslayoutconstraint constraintswithvisualformat:@"h:[_changekeyboardbutton(33)]-43-|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_changekeyboardbutton)];
 [self addconstraints:changekeyboardbuttonh];
 
 nsarray *changekeyboardbuttonv = [nslayoutconstraint constraintswithvisualformat:@"v:|-5-[_changekeyboardbutton(33)]" options:0 metrics:0 views:nsdictionaryofvariablebindings(_changekeyboardbutton)];
 [self addconstraints:changekeyboardbuttonv];
 
 
 //给文本框添加约束
 self.sendtextview.translatesautoresizingmaskintoconstraints = no;
 nsarray *sendtextviewconstrainth = [nslayoutconstraint constraintswithvisualformat:@"h:|-45-[_sendtextview]-80-|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_sendtextview)];
 [self addconstraints:sendtextviewconstrainth];
 
 nsarray *sendtextviewconstraintv = [nslayoutconstraint constraintswithvisualformat:@"v:|-10-[_sendtextview]-10-|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_sendtextview)];
 [self addconstraints:sendtextviewconstraintv];
 
 
 //语音发送按钮
 self.sendvoicebutton.translatesautoresizingmaskintoconstraints = no;
 nsarray *sendvoicebuttonconstrainth = [nslayoutconstraint constraintswithvisualformat:@"h:|-50-[_sendvoicebutton]-90-|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_sendvoicebutton)];
 [self addconstraints:sendvoicebuttonconstrainth];
 
 nsarray *sendvoicebuttonconstraintv = [nslayoutconstraint constraintswithvisualformat:@"v:|-6-[_sendvoicebutton]-6-|" options:0 metrics:0 views:nsdictionaryofvariablebindings(_sendvoicebutton)];
 [self addconstraints:sendvoicebuttonconstraintv];
 
 
}

5.因为我们要发送录音,所以对音频部分的初始化是少不了的,以下代码是对音频的初始化

//录音部分初始化
-(void)audioinit
{
 nserror * err = nil;
 
 avaudiosession *audiosession = [avaudiosession sharedinstance];
 [audiosession setcategory :avaudiosessioncategoryplayandrecord error:&err];
 
 if(err){
  nslog(@"audiosession: %@ %d %@", [err domain], [err code], [[err userinfo] description]);
  return;
 }
 
 [audiosession setactive:yes error:&err];
 
 err = nil;
 if(err){
  nslog(@"audiosession: %@ %d %@", [err domain], [err code], [[err userinfo] description]);
  return;
 }

 //通过可变字典进行配置项的加载
 nsmutabledictionary *setaudiodic = [[nsmutabledictionary alloc] init];
 
 //设置录音格式(aac格式)
 [setaudiodic setvalue:@(kaudioformatmpeg4aac) forkey:avformatidkey];
 
 //设置录音采样率(hz) 如:avsampleratekey==8000/44100/96000(影响音频的质量)
 [setaudiodic setvalue:@(44100) forkey:avsampleratekey];
 
 //设置录音通道数1 or 2
 [setaudiodic setvalue:@(1) forkey:avnumberofchannelskey];
 
 //线性采样位数 8、16、24、32
 [setaudiodic setvalue:@16 forkey:avlinearpcmbitdepthkey];
 //录音的质量
 [setaudiodic setvalue:@(avaudioqualityhigh) forkey:avencoderaudioqualitykey];
 
 nsstring *strurl = [nssearchpathfordirectoriesindomains(nsdocumentdirectory, nsuserdomainmask, yes) lastobject];
 
 nsstring *filename = [nsstring stringwithformat:@"%ld", (long)[[nsdate date] timeintervalsince1970]];
 
 
 nsurl *url = [nsurl fileurlwithpath:[nsstring stringwithformat:@"%@/%@.aac", strurl, filename]];
 _audioplayurl = url;
 
 nserror *error;
 //初始化
 self.audiorecorder = [[avaudiorecorder alloc]initwithurl:url settings:setaudiodic error:&error];
 //开启音量检测
 self.audiorecorder.meteringenabled = yes;
 self.audiorecorder.delegate = self;

}

6.添加键盘回收键done

//给键盘添加done键
-(void) adddone
{
 //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.sendtextview.inputaccessoryview =toolbar;
}

三.编写控件的回调方法
控件添加好以后下面要添加触发控件要干的事情:
1.从最复杂的开始,长按发送录音的按钮时,会录音。松开收时会发送(在发送时要判断音频的时间,太小不允许发送)。录音时上滑取消录音(删除录音文件)。主要是给录音按钮加了一个longpress手势,根据手势的状态来做不同的事情。关于手势的内容请参考之前的博客:(ios开发之手势识别),下面是录音业务逻辑的实现(个人在coding的时候,感觉这一块是工具条中最复杂的部分),代码如下:  

//长按手势触发的方法
-(void)sendvoicebuttonlongpress:(id)sender
{
 static int i = 1;
 if ([sender iskindofclass:[uilongpressgesturerecognizer class]]) {
  
  uilongpressgesturerecognizer * longpress = sender;
  
  //录音开始
  if (longpress.state == uigesturerecognizerstatebegan)
  {
   
   i = 1;
   
   [self.sendvoicebutton settitlecolor:[uicolor redcolor] forstate:uicontrolstatenormal];
   //录音初始化
   [self audioinit];
   
   //创建录音文件,准备录音
   if ([self.audiorecorder preparetorecord])
   {
    //开始
    [self.audiorecorder record];
    
    //设置定时检测音量变化
    _timer = [nstimer scheduledtimerwithtimeinterval:0.05 target:self selector:@selector(detectionvoice) userinfo:nil repeats:yes];
   }
  }
  
  
  //取消录音
  if (longpress.state == uigesturerecognizerstatechanged)
  {
   
   cgpoint piont = [longpress locationinview:self];
   nslog(@"%f",piont.y);

   if (piont.y < -20)
   {
    if (i == 1) {
     
     [self.sendvoicebutton setbackgroundimage:[uiimage imagenamed:@"chat_bottom_textfield.png"] forstate:uicontrolstatenormal];
     [self.sendvoicebutton settitlecolor:[uicolor blackcolor] forstate:uicontrolstatenormal];
     //删除录制文件
     [self.audiorecorder deleterecording];
     [self.audiorecorder stop];
     [_timer invalidate];
     
     uialertview *alter = [[uialertview alloc] initwithtitle:@"提示" message:@"录音取消" delegate:nil cancelbuttontitle:@"取消" otherbuttontitles: nil];
     [alter show];
     //去除图片用的
     self.cancelblock(1);
     i = 0;
     
    }

    
   }
   }
  
  if (longpress.state == uigesturerecognizerstateended) {
   if (i == 1)
   {
    nslog(@"录音结束");
    [self.sendvoicebutton setbackgroundimage:[uiimage imagenamed:@"chat_bottom_textfield.png"] forstate:uicontrolstatenormal];
    [self.sendvoicebutton settitlecolor:[uicolor blackcolor] forstate:uicontrolstatenormal];
    
    double ctime = self.audiorecorder.currenttime;
    if (ctime > 1)
    {
     //如果录制时间<2 不发送
     nslog(@"发出去");
     self.urlblock(self.audioplayurl);
    }
    else
    {
     //删除记录的文件
     [self.audiorecorder deleterecording];
     uialertview *alter = [[uialertview alloc] initwithtitle:@"提示" message:@"录音时间太短!" delegate:nil cancelbuttontitle:@"取消" otherbuttontitles: nil];
     [alter show];
     self.cancelblock(1);
     
    }
    [self.audiorecorder stop];
    [_timer invalidate];
   }
  }
  
  
 }
 
}

2.下面的代码是检测音量的变化,用于根据音量变化图片,代码如下:

//录音的音量探测
- (void)detectionvoice
{
 [self.audiorecorder updatemeters];//刷新音量数据
 //获取音量的平均值 [recorder averagepowerforchannel:0];
 //音量的最大值 [recorder peakpowerforchannel:0];
 
 cgfloat lowpassresults = pow(10, (0.05 * [self.audiorecorder peakpowerforchannel:0]));
 
 //把声音的音量传给调用者
 self.volumeblock(lowpassresults);
}

3.轻击输入框时,切换到系统键盘,代码如下:

//轻击sendtext切换键盘
-(void)tapgesture:(uitapgesturerecognizer *) sender
{
 if ([self.sendtextview.inputview isequal:self.functionview])
 {
  self.sendtextview.inputview = nil;
  
  [self.changekeyboardbutton setimage:[uiimage imagenamed:@"chat_bottom_smile_nor.png"] forstate:uicontrolstatenormal];
  
  [self.sendtextview reloadinputviews];
 }
 
 if (![self.sendtextview isfirstresponder])
 {
  [self.sendtextview becomefirstresponder];
 }
}

4.通过输入框的文字多少改变toolview的高度,因为输入框的约束是加在toolview上的,所以需要把输入框的contentsize通过block传到toolview的调用者上,让toolview的父视图来改变toolview的高度,从而sendtextview的高度也会随着改变的,下面的代码是把contentsize交给父视图:代码如下:

//通过文字的多少改变toolview的高度
-(void)textviewdidchange:(uitextview *)textview
{
 cgsize contentsize = self.sendtextview.contentsize;
 
 self.sizeblock(contentsize);
}

效果如下,文字多时textview的高度也会增大:

iOS开发之微信聊天工具栏的封装

5.点击最左边的按钮触发的事件(切换文本输入框和录音按钮),代码如下:

//切换声音按键和文字输入框
-(void)tapvoicechangebutton:(uibutton *) sender
{

 if (self.sendvoicebutton.hidden == yes)
 {
  self.sendtextview.hidden = yes;
  self.sendvoicebutton.hidden = no;
  [self.voicechangebutton setimage:[uiimage imagenamed:@"chat_bottom_keyboard_nor.png"] forstate:uicontrolstatenormal];
  
  if ([self.sendtextview isfirstresponder]) {
   [self.sendtextview resignfirstresponder];
  }
 }
 else
 {
  self.sendtextview.hidden = no;
  self.sendvoicebutton.hidden = yes;
  [self.voicechangebutton setimage:[uiimage imagenamed:@"chat_bottom_voice_press.png"] forstate:uicontrolstatenormal];
  
  if (![self.sendtextview isfirstresponder]) {
   [self.sendtextview becomefirstresponder];
  }
 }
}

6.点击return发送文字(通过block回调传入到父视图上),代码如下:

//发送信息(点击return)
- (bool)textview:(uitextview *)textview shouldchangetextinrange:(nsrange)range replacementtext:(nsstring *)text
{
 if ([text isequaltostring:@"\n"])
 {
  
  //通过block回调把text的值传递到controller*
  self.textblock(self.sendtextview.text);
  
  self.sendtextview.text = @"";
  
  return no;
 }
 return yes;
}

7.录音按钮本身要做的事情(在longpress没有被触发时调用)代码如下:

//发送声音按钮回调的方法
-(void)tapsendvoicebutton:(uibutton *) sender
{
 nslog(@"sendvoicebutton");
 //点击发送按钮没有触发长按手势要做的事儿
 uialertview *alter = [[uialertview alloc] initwithtitle:@"提示" message:@"按住录音" delegate:nil cancelbuttontitle:@"取消" otherbuttontitles: nil];
 [alter show];
}

8.调用表情键盘:

//变成表情键盘
-(void)tapchangekeyboardbutton:(uibutton *) sender
{
 if ([self.sendtextview.inputview isequal:self.functionview])
 {
  self.sendtextview.inputview = nil;
  
  [self.changekeyboardbutton setimage:[uiimage imagenamed:@"chat_bottom_smile_nor.png"] forstate:uicontrolstatenormal];
  
  [self.sendtextview reloadinputviews];
 }
 else
 {
  self.sendtextview.inputview = self.functionview;
  
  
  [self.changekeyboardbutton setimage:[uiimage imagenamed:@"chat_bottom_keyboard_nor.png"] forstate:uicontrolstatenormal];
  
  [self.sendtextview reloadinputviews];
 }
 
 if (![self.sendtextview isfirstresponder])
 {
  [self.sendtextview becomefirstresponder];
 }
 
 if (self.sendtextview.hidden == yes) {
  self.sendtextview.hidden = no;
  self.sendvoicebutton.hidden = yes;
  [self.voicechangebutton setimage:[uiimage imagenamed:@"chat_bottom_voice_press.png"] forstate:uicontrolstatenormal];
  
 }

}

以上就是toolview的所有封装代码,至于在controller中如何使用他来发送消息,如何定义聊天cell,如何处理录音文件,聊天时的气泡是如何实现的等功能,在以后的文章中会继续讲解,希望大家继续关注。