用户登录
用户注册

分享至

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

  • 作者: 猥琐大欧巴
  • 来源: 51数据库
  • 2021-10-12

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

在封装聊天工具条的的时候表情键盘是之前封装好的,所以拿过来就可以用的啦。因为不管是工具条还是表情键盘都是用约束来控件大小的,所以横屏也是没问题的,在大屏手机上也是没问题的。下面将会一步步讲解如何封装下面的聊天工具条。主要是对工具条的封装,表情键盘在这就不做讲解了。
一、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的高度也会增大:

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,如何处理录音文件,聊天时的气泡是如何实现的等功能,在以后的文章中会继续讲解,希望大家继续关注。

软件
前端设计
程序设计
Java相关