仿微博客户端一条微博的布局

前言

做一个微博客户端的第三方是自学的第一个实践的项目,自从从事iOS工作之后,就把这个项目给搁置了。趁现在过年回来有些空闲时间,再次修改(总觉得项目就是不停地修改)。并且记录一点东西,以后可再回头看看从前走过的路,挖过的坑。这是一条微博的展示,不是整个项目。

废话不多说,先上效果图:


仿微博布局.png

拆分控件

在开始动手写代码之前,我们得先确定怎么去实现这样子的布局,也就是分析需要用到哪些控件。

  • 观察微博客户端,整体是可滑动的,而且界面展示比较规律的,所以应该是使用UITableView实现的。那么一条微博应该是用UITableViewCell 来实现的,这个由点击时,整条微博都变色可以肯定。

  • 一条微博与其他的微博之间是有大约10PX的间距,可以认为每个Section就只有一个Cell。

  • 每条微博的共同部分包括:头像,用户名称,发布时间与发布来源,微博正文,底部的转发,评论,赞。不同的部分有:配图,非原创微博的正文。(视频,文章等在这个项目中不做考虑)所以共同部分可以直接在xib上固定,不同部分则需要在.m文件用代码来写。

  • 控件的确定:头像和配图使用UIImageView,用户名称,发布时间与来源,微博正文,非原创微博的正文都是使用UILabel,而底部的转发,评论,赞使用UIButton。

  • 当一条微博是非原创微博(转发微博),根据点击被转发的微博的变色情况,可以确定转发微博是一个整体,可以确定转发微博是放在一个UIView上再添加到Cell上面的。

布局

放上一张xib的布局图:(button是与底部进行约束的)


xib.png

共同的部分,先设置一些参数。

- (void)awakeFromNib {
[super awakeFromNib];
_contentLabel.numberOfLines = 0;//正文多行

//圆形头像
_headImageView.layer.masksToBounds = YES;
_headImageView.layer.cornerRadius = HeadImageHeight / 2;

//设置tag,为了后面识别点击哪个按钮
_repostButton.tag = RepostButtonTag;
_commentButton.tag = CommentButtonTag;
_likeButton.tag = LikeButtonTag;
}

先说配图,微博的配图最多9张。首先先根据配图的张数和屏幕的宽度确定图片的大小imageWidth,然后再确定行数和列数。

  1. 只有一张配图时,imageWidth = 屏幕宽度 * 0.55;

  2. 配图超过一张时,imageWidth = (屏幕宽度 - 间隙) / 3;

  3. 配图是2张或者4张时,分为两列布局,而配图3张或者大于4张时,则分为三列布局。
    LeadingSpace 是图片与两侧屏幕的间隙,为8PX, ImageSpace是图片之间的间隙为4PX。UI_SCREEN_WIDTH是屏幕宽度。

      //根据图片数量获得列数
     if (_imageArray.count == 1) {
     //一列
     column = 1;
     imageWidth = UI_SCREEN_WIDTH * 0.55;
     }
     else if (_imageArray.count == 2 || _imageArray.count == 4) {
     //两列
     column = 2;
     imageWidth = (UI_SCREEN_WIDTH - (LeadingSpace + ImageSpace) * 2) / 3;
     }
     else {
     //三列
     column = 3;
     imageWidth = (UI_SCREEN_WIDTH - (LeadingSpace + ImageSpace) * 2) / 3;
     }
    
     //根据图片的数量和列数获得行数
     if (_imageArray.count % column == 0) {
     row = _imageArray.count / column;
     }
     else {
     row = _imageArray.count / column + 1;
     }
    

确定了配图的大小,再根据位置,就可以创建UIImageView。 配图的位置则在正文起始位置 + 正文的高度 + 间隙,而获取正文的高度由以下方法来完成:

 *  计算label的高度
 *
 *  @param text  文字
 *  @param width label宽度
 *  @param font  字体
 *
 *  @return label高度
+ (CGFloat)getLabelHeightWithText:(NSString *)text width:(CGFloat)width font:(UIFont *)font {
CGSize size = CGSizeMake(width, MAXFLOAT);//设置一个行高的上限
CGSize returnSize;

NSDictionary *attribute = @{ NSFontAttributeName : font };
returnSize = [text boundingRectWithSize:size
                                options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
                             attributes:attribute
                                context:nil].size;

return returnSize.height;
}

对于原创微博正文的起始位置可以由xib看出来,头像的高度固定为48,而上下的间隙为8, 则起始位置Y坐标为 48 + 16 = 64;而对于非原创微博,正文的起始位置Y坐标为8 (此处的8是相对于配图的父容器UIView的位置,对于非原创微博而言,更重要的是计算出父容器UIView在Cell中的位置);
然后根据配图的位置和大小创建UIImageView,如下图,其中originY为第一张配图的起始位置的Y坐标。

//根据位置创建imageView
for (int i = 0; i < row; i++) {
    for (int j = 0; j < column; j++) {
        //用来判断数据是否越界
        if (i * column + j < _imageArray.count) {
            imageUrl = _imageArray[i * column + j];
            
            if (imageUrl) {
                TapImageView *imageView = [[TapImageView alloc] initWithFrame:CGRectMake(LeadingSpace + j * (ImageSpace + imageWidth), originY + LeadingSpace + i * (ImageSpace + imageWidth), imageWidth, imageWidth)];
                imageView.tag = ImageViewTag + i * column + j;
                
                //block通知,点击了图片,展示大图
                __weak typeof(self) weakSelf = self;
                imageView.didTouchImage = ^(NSInteger index) {
                    [weakSelf showFullScreenImage:index];
                };
                
                [imageView setImageUrl:imageUrl index:i * column + j];

                //原创微博直接添加的cell中,非原创则加入一个容器中UIView,再将容器加入cell中
                if (isForward) {
                    [_forwardedContainerView addSubview:imageView];
                }
                else {
                    [self addSubview:imageView];
                }
            }
        }
        else {
            //越界后跳出for循环
            break;
        }
    }
}

TapImageView是UIImageView的子类,主要是添加了手势,实现点击图片展开大图的效果,后面再做详细介绍。

非原创微博有两个正文,分别用“上文”和“下文”来区分吧。上文已经在xib中,而下文和配图是放在_forwardedContainerView(UIView)中,然后再添加到Cell中的,所以要计算它的起始位置Y坐标。上文的Y坐标已经确定为64了,而_forwardedContainerView与上文之间的间隙为8,所以下文的Y坐标 = 64 + 上文的高度 + 8。其中ContentLabelOriginY = 64

CGFloat contentHeight = [FitBoUI getLabelHeightWithText:_weibo.text width:UI_SCREEN_WIDTH - LeadingSpace * 2 font:FontSize12];
CGFloat originY = ContentLabelOriginY + contentHeight;
originY += LeadingSpace;
_forwardedContainerView = [[UIView alloc] initWithFrame:CGRectMake(0, originY, UI_SCREEN_WIDTH, 40)];
_forwardedContainerView.tag = ForwardedContainerViewTag;
_forwardedContainerView.backgroundColor = [UIColor colorWithWhite:0.75 alpha:0.35];

//添加单击手势,点击原创微博,进入该微博的详情页面
[self forwardedContainerViewAddGesture];
[self addSubview:_forwardedContainerView];

_forwardedContainerView的高度是随便给的,需要在计算实际高度之后再重新赋值。

//下文是用户名称和文字拼凑而来。
NSString *forwardText = [NSString stringWithFormat:@"@%@:%@", forwardWeibo.user.name, forwardWeibo.text];
CGFloat forwardContentHeight = [FitBoUI getLabelHeightWithText:forwardText width:UI_SCREEN_WIDTH - LeadingSpace * 2 font:FontSize12];
UILabel *forwardedContentLabel = [[UILabel alloc] initWithFrame:CGRectMake(LeadingSpace, LeadingSpace, UI_SCREEN_WIDTH - LeadingSpace * 2, forwardContentHeight)];
forwardedContentLabel.font = FontSize12;
forwardedContentLabel.numberOfLines = 0;
forwardedContentLabel.text = forwardText;

[_forwardedContainerView addSubview:forwardedContentLabel];

//创建imageview,并根据修改实际高度,pic_urls是图片的网址数组。得到的imageHeight为所有图片以及图片之间的间隙总和。
CGFloat imageHeight = [self initImageView:forwardWeibo.pic_urls originY:forwardContentHeight + LeadingSpace isForward:YES];
//此处无论有没有配图,都预留了配图上下两个间隙的高度。所以,如果没有配图,上面返回的imageHeight = - LeadingSpace才合适。
_forwardedContainerView.frame = CGRectMake(0, originY, UI_SCREEN_WIDTH, forwardContentHeight + imageHeight + LeadingSpace * 3);

TapImageView.h文件

#import <UIKit/UIKit.h>
@interface TapImageView : UIImageView
@property (copy, nonatomic) void (^didTouchImage)(NSInteger index);

- (instancetype)initWithFrame:(CGRect)frame;

/**
 设置图片地址

 @param url   图片地址
 @param index 图片下标
 */
- (void)setImageUrl:(NSString *)url index:(NSInteger)index;
@end

TapImageView.m文件

#import "TapImageView.h"
#import "UIImageView+WebCache.h"

@interface TapImageView ()
@property (assign, nonatomic) NSInteger index;
@end

@implementation TapImageView

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];

if (self) {
    [self initView];
}

return self;
}

- (void)initView {
//添加单击手势
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageViewTapAction:)];
gesture.numberOfTapsRequired = 1;
self.userInteractionEnabled = YES;

[self addGestureRecognizer:gesture];
}

//发送点击图片的通知,并传回下标
- (void)imageViewTapAction:(UITapGestureRecognizer *)gesture {
if (_didTouchImage) {
    _didTouchImage(_index);
}
}

/**
 设置图片地址

 @param url   图片地址
 @param index 图片下标
 */
- (void)setImageUrl:(NSString *)url index:(NSInteger)index {
if (url) {
    [self sd_setImageWithURL:[NSURL URLWithString:url]];
}

_index = index;
}

在Cell中,会根据传回的点击图片下标展示相应图片的大图。

注意:

因为下文和配图等是运行时动态添加上去的,而cell是复用的,则每次使用cell的时候,需要将它们先移除。如果没有移除,则复用cell的时候就会发生cell位置错乱的情况。

- (void)removeView {
//移除转发微博
for (UIView *view in self.subviews) {
    if (view.tag == ForwardedContainerViewTag) {
        [view removeFromSuperview];
        
        break;
    }
}

//移除图片
for (UIView *view in self.subviews) {
    if ([view isKindOfClass:[TapImageView class]]) {
        [view removeFromSuperview];
    }
}
}

在控制器中的实现

在控制器的xib中只有一个UITableView,可以直接在xib中指定UITableView的dataSource 和delegate,也可以在.m文件中再指定。

//注册cell,WeiboCellIdentifier是cell复用时用到的
UINib *weiboNib = [UINib nibWithNibName:@"FitBoCell" bundle:nil];
[_mainTableView registerNib:weiboNib forCellReuseIdentifier:WeiboCellIdentifier];

//移除分割线
_mainTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_mainTableView.delegate = self;
_mainTableView.dataSource = self;

接着实现UITableViewDataSource, UITableViewDelegate里面的方法。

//返回section的个数
  - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return _weiboArray.count;
}

//返回每个section里面的行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
//返回每个section底部的高度,默认为20PX, 就是如果不实现该方法或者return 0,实际都是返回20PX
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return 0.001;
}
//返回每个section头部的高度,默认为20PX, 就是如果不实现该方法或者return 0,实际都是返回20PX
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 10;
}
//返回每一行的高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger section = indexPath.section;
WeiboModel *weibo = _weiboArray[section];

return [FitBoCell getCellHeight:weibo];
}

//在这个方法里面设置cell的内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger section = indexPath.section;
//这个办法是模型转化,利用MJExtension框架
 WeiboModel *weibo = [WeiboModel mj_objectWithKeyValues:_weiboArray[section]];
FitBoCell *cell = [tableView dequeueReusableCellWithIdentifier:WeiboCellIdentifier];

if (cell == nil) {
    cell = [[FitBoCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:WeiboCellIdentifier];
}

//这里是点击非原创微博里面的原创微博的回调,也就是_forwardedContainerView的点击回调
__weak typeof(self) weakSelf = self;
cell.didTouchForwardedWeibo = ^(WeiboModel *weibo) {
    //跳转到微博的详情页面
    [weakSelf forwardedWeiboTouch:weibo];
};

[cell setWeiboInfo:weibo];

return cell;
}

//cell的点击响应事件,跳转到微博的详情页面
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];

NSInteger section = indexPath.section;
WeiboModel *weibo = [WeiboModel mj_objectWithKeyValues:_weiboArray[section]];

CommentOrRepostListViewController *listVC = [CommentOrRepostListViewController new];
[listVC setWeibo:weibo offset:NO];

[self.navigationController pushViewController:listVC animated:YES];
}

其中,因为cell的高度是根据实际情况不定的,所以使用了类方法来获取。[FitBoCell getCellHeight:weibo]

/**
 *  获取cell的高度
 *
 *  @param weibo weibo
 *
 *  @return height
 */
+ (CGFloat)getCellHeight:(WeiboModel *)weibo {
CGFloat contentHeight = [FitBoUI getLabelHeightWithText:weibo.text width:UI_SCREEN_WIDTH - LeadingSpace * 2 font:FontSize12];
CGFloat originY = ContentLabelOriginY + contentHeight + LeadingSpace;

if (weibo.retweeted_status == nil) {
    //原创微博
    CGFloat imageHeight = [self getImageHeight:weibo.pic_urls.count];
    return originY + imageHeight + LeadingSpace + ButtonHeight;
}
else {
    //非原创微博
    WeiboModel *forwardWeibo = weibo.retweeted_status;
    NSString *forwardText = [NSString stringWithFormat:@"@%@:%@", forwardWeibo.user.name, forwardWeibo.text];
    CGFloat imageHeight = [self getImageHeight:forwardWeibo.pic_urls.count];
    CGFloat forwardContentHeight = [FitBoUI getLabelHeightWithText:forwardText width:UI_SCREEN_WIDTH - LeadingSpace * 2 font:FontSize12];
    
    return originY + LeadingSpace + forwardContentHeight + imageHeight + LeadingSpace * 2 + ButtonHeight;
}
}

//获取图片的整体高度
+ (CGFloat)getImageHeight:(NSInteger)count {
if (count < 1) {
   //上面计算高度的时候预留了配图上下两个间隙的高度。所以,如果没有配图,返回 - LeadingSpace才合适。
    return - LeadingSpace;
}
else if (count == 1) {
    return UI_SCREEN_WIDTH * 0.55;
}
else if (count / 3 < 1 || count == 3) {
    //一行
    return (UI_SCREEN_WIDTH - (LeadingSpace + ImageSpace) * 2) / 3;
}
else if (count > 3 && count <= 6) {
    //两行
    return (UI_SCREEN_WIDTH - (LeadingSpace + ImageSpace) * 2) / 3 * 2 + ImageSpace;
}
else {
    //三行
    return (UI_SCREEN_WIDTH - (LeadingSpace + ImageSpace) * 2) + ImageSpace * 2;
}
}

其他的点击事件的响应方法等,就不累赘了。最后再放一张非原创微博的效果图:

非原创微博.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容