• (二十)即时通信的聊天气泡的实现I


    Tip:通过xib和storyboard不可能将一个控件作为ImageView的子控件,只能通过代码的addSubview方法实现。

    设置图片的细节:如果button比图片大(为了方便对齐),将图片设置为image而不是background,图片不会被拉伸到失真。

    为了保证在不同系统上显示的效果一样,可以不使用系统默认样式,用自定义的背景等,例如QQ的聊天框,如果要实现,首先将TextField的BorderStyle选为空:


    然后设置自己的background即可。

    细节:聊天的type(发送的还是接受的)应该用什么数据类型?

    为了降低沟通成本,提高可读性和安全性,应该使用枚举。

    枚举类型命名规范:类名+属性名(首字母大写)。枚举成员也要类名+属性为前缀。

    typedef enum{
        MessageTypeMe = 0,
        MessageTypeOther
    } MessageType ;
    @property (nonatomic, assign) MessageType type;
    Tip:使用KVC的时候,枚举会自动转整形。

    注意弱指针不能指向alloc的对象,否则会被直接销毁,应该先用强指针指着alloc的对象,然后加入到父控件,最后再用弱指针指过去:

    @property (nonatomic, weak) UILabel *timeView;
    UILabel *timeView = [[UILabel alloc] init];
    [self.contentView addSubview:timeView];
    self.timeView = timeView;
    自定义cell的步骤:

    第一步:新建一个继承自UITableViewCell的类

    第二步:重写initWithStyle:reuseIdentifier方法

    添加所有的子控件,不需要设置数据和frame(声明一个frame属性以便设置),加入到self.contentView中。一定注意弱指针的用法,先用强指针,加入视图后再交给弱指针。

    @interface MessageCell : UITableViewCell
    
    @property (nonatomic, strong) MessageFrame *messageFrame;
    
    + (instancetype)cellWithTableView:(UITableView *)tableView;
    
    @end

    @interface MessageCell ()
    
    @property (nonatomic, weak) UILabel *timeView;
    
    @property (nonatomic, weak) UIImageView *iconView;
    
    @property (nonatomic, weak) UIButton *textView;
    
    @end
    
    @implementation MessageCell
    
    - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
        self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
        if (self) {
            //时间:label
            //头像:imgaeView
            //正文:button
            UILabel *timeView = [[UILabel alloc] init];
            [self.contentView addSubview:timeView];
            self.timeView = timeView;
            
            UIImageView *iconView = [[UIImageView alloc] init];
            [self.contentView addSubview:iconView];
            self.iconView = iconView;
            
            UIButton *textView = [[UIButton alloc] init];
            [self.contentView addSubview:textView];
            self.textView = textView;
        }
        return self;
    }
    
    - (void)awakeFromNib {
        // Initialization code
    }
    
    - (void)setSelected:(BOOL)selected animated:(BOOL)animated {
        [super setSelected:selected animated:animated];
    
        // Configure the view for the selected state
    }
    
    @end
    

    细节:在cell类内写一个类方法用于实现缓存池的性能优化:

    + (instancetype)cellWithTableView:(UITableView *)tableView{
        
        static NSString *ID = @"message";
        MessageCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
        if(cell == nil){
            cell = [[MessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
        }
        return cell;
        
    }
    这样就大大简化了返回cell的代码:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
        
        MessageCell *cell = [MessageCell cellWithTableView:tableView];
        return cell;
        
    }

    第三步:提供数据模型和frame模型(后者存放数据模型和所有子控件的高度、cell的高度)

    Tip:要使用CGRect,首先要引入<UIKit/UIKit.h>。

    注意frame应该是只读的,只在模型内计算,没有set方法,所以类内使用下划线访问。

    对于数据模型message,主要是提供消息的类型、事件、内容:

    typedef enum{
        MessageTypeMe = 0,
        MessageTypeOther
    } MessageType ;
    
    @interface Message : NSObject
    
    @property (nonatomic, copy) NSString *text;
    
    @property (nonatomic, copy) NSString *time;
    
    @property (nonatomic, assign) MessageType type;
    
    - (instancetype)initWithDict:(NSDictionary *)dict;
    + (instancetype)messageWithDict:(NSDictionary *)dict;
    
    @end
    和以前的模型声明和实现完全一样。

    对于messageFrame模型,内部的数据为Cell各部分的尺寸和message模型,最后控制器访问的将是这个模型。

    #import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>
    
    #define NameFont [UIFont systemFontOfSize:14]
    #define TextFont [UIFont systemFontOfSize:15]
    
    @class Message;
    
    @interface MessageFrame : NSObject
    /**
     *  头像的Frame
     */
    @property (nonatomic, assign, readonly) CGRect iconF;
    /**
     *  时间的Frame
     */
    @property (nonatomic, assign, readonly) CGRect timeF;
    /**
     *  正文的Frame
     */
    @property (nonatomic, assign, readonly) CGRect textF;
    /**
     *  cell的高度
     */
    @property (nonatomic, assign, readonly) CGFloat cellHeight;
    /**
     *  数据模型
     */
    @property (nonatomic, strong) Message *message;
    
    @end

    需要注意的是,Frame属性只在模型内计算,不允许在外部修改,声明为readonly,为了使用CGXxx要引入UIKit框架。

    计算Frame的时机应该是在控制器将message信息传入的时候,因此要重写message的set方法来计算尺寸:

    计算的过程略为繁琐,总之就是实现类似QQ的聊天起泡效果,这里只是计算了时间、消息框、头像的位置和尺寸,并没有加上起泡。

    注意一个细节,使用UIScreen的mainScreen方法的bounds.size得到屏幕尺寸。

    - (void)setMessage:(Message *)message{
        
        _message = message;
        
        CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
        CGFloat padding = 10;
        
        CGFloat timeX = 0;
        CGFloat timeY = 0;
        CGFloat timeW = 320;
        CGFloat timeH = 40;
        
        _timeF = CGRectMake(timeX, timeY, timeW, timeH);
        CGFloat iconX;
        CGFloat iconY = CGRectGetMaxY(_timeF);
        CGFloat iconW = 40;
        CGFloat iconH = 40;
        if (message.type == MessageTypeMe) {
            iconX = screenW - padding - iconW;
        }else{
            iconX = padding;
        }
        _iconF = CGRectMake(iconX, iconY, iconW, iconH);
        
        CGSize textMaxSize = CGSizeMake(150, MAXFLOAT);
        CGSize textSize = [self sizeWithText:message.text font:TextFont maxSize:textMaxSize];
        CGFloat textY = iconY;
        CGFloat textX;
        if (message.type == MessageTypeMe) {
            textX = iconX - padding - textSize.width;
        }else{
            textX = CGRectGetMaxX(_iconF) + padding;
        }
        //_textF = CGRectMake(textX, textY, textSize.width, textSize.height);
        _textF = (CGRect){{textX,textY},textSize};
        
        CGFloat textMaxY = CGRectGetMaxY(_textF);
        CGFloat iconMaxY = CGRectGetMaxY(_iconF);
        _cellHeight = MAX(textMaxY, iconMaxY);
        
    }

    其中在计算文字对应的text尺寸时,使用了如下的方法:

    - (CGSize)sizeWithText:(NSString *)text font:(UIFont *)font maxSize:(CGSize)maxSize{
        NSDictionary *attrs = @{NSFontAttributeName : font};
        return [text boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attrs context:nil].size;
    }

    最后剩下的最重要的问题,是为cell的子控件设置frame模型的时机,可以注意到,在通过initWithTableView获取到cell之后,应该对cell的messageFrame属性进行设置,所以只要重写cell的messageFrame的set方法即可在这个时候改变控件的属性,然后得到正确的cell。

    - (void)setMessageFrame:(MessageFrame *)messageFrame{
        
        _messageFrame = messageFrame;
        
        Message *msg = messageFrame.message;
        
        self.timeView.text = msg.time;
        self.timeView.frame = messageFrame.timeF;
        
        NSString *icon = msg.type == MessageTypeMe ? @"me" : @"other";
        self.iconView.image = [UIImage imageNamed:icon];
        self.iconView.frame = messageFrame.iconF;
        
        [self.textView setTitle:msg.text forState:UIControlStateNormal];
        self.textView.frame = messageFrame.textF;
        
    }

    对于要多次计算的数据,放在message的set方法内计算,对于一次性的计算(例如Cell的背景色),放在Cell的init方法中计算:

    改良后的Cell初始化方法:注意一个细节,clearColor为透明色(无色)。

    - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
        self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
        if (self) {
            //一次性的修改放到init方法中
            //时间:label
            //头像:imgaeView
            //正文:button
            UILabel *timeView = [[UILabel alloc] init];
            timeView.textAlignment = NSTextAlignmentCenter;
            timeView.textColor = [UIColor grayColor];
            [self.contentView addSubview:timeView];
            self.timeView = timeView;
            
            UIImageView *iconView = [[UIImageView alloc] init];
            [self.contentView addSubview:iconView];
            self.iconView = iconView;
            
            UIButton *textView = [[UIButton alloc] init];
            textView.titleLabel.numberOfLines = 0; //自动换行
            textView.titleLabel.font = TextFont;
            textView.backgroundColor = [UIColor grayColor];
            [self.contentView addSubview:textView];
            self.textView = textView;
            
            //设置cell的背景色
            self.backgroundColor = [UIColor clearColor];
        }
        return self;
    }
    



    下面的步骤就和以前一样,控制器新建messageFrames,懒加载数据,然后TableView根据数据源和委托加载数据。


    下面总结一下调用过程:

    1.控制器加载messageFrames,需要为每一个messageFrame的message属性赋值-

    2.由于重写了message的set方法,在set方法内部根据message计算得到文字的宽高,进而得到头像、时间位置尺寸,确定所有的Frame,并且存住message(保存之前先用message的类方法字典转模型)。

    3.系统调用获取Cell的方法时,由于调用了被重写的构造方法initWithStyle:style reuseIdentifier: ,在其内部通过性能优化取得Cell,然后创建各个子控件,进行一次性属性的设置,最后返回Cell。

    4.Cell内部有messageFrame属性需要设置,在设置该属性时,会调用重写的set方法,在这个方法内部,实现了对各个子控件尺寸根据传入的messageFrame修改的操作。

    5.Cell经过以上4步被正确的创建和设置,返回后正确的显示。

    调用顺序:Message类(字典转Message模型)->Frame类(设置messageFrame的message时调用set方法)->MessageCell类(初始化Cell)->MessageCell类(设置Cell的messageFrame属性时调用set方法)。




  • 相关阅读:
    反转链表——临时变量的妙用
    C++指针学习(2)
    统计英文文本中的词频
    灵活的C++
    编程之美 NIM(1)扩展问题
    深度探索C++对象模型读书笔记(1)
    irrlicht1.7.0(2):基础接口
    关于裁剪空间与投影变换矩阵的推导
    irrlicht1.7.0(1):irrTypes.h
    【转】每天拿两个小时来浪费(文/王路)
  • 原文地址:https://www.cnblogs.com/aiwz/p/6154233.html
Copyright © 2020-2023  润新知