CoreText 实现图文混排(附Demo)

CoreText简介

处理文字和字体的底层技术。它直接和Core Graphics打交道,是iOS和OSX底层的告诉二维图像渲染引擎。Quartz能够直接处理字体和字形,将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此CoreText为了排版,需要将显示的文本内容、位置、字体等信息传递给Quartz。与其他组件相比,具有更高效的排版功能。

UIWebView也可以作为处理复杂的文字排版的备选方案。两者之间的比较:

1.CoreText占用内存更少,渲染速度更快;UIWebView占用内存更多,渲染速度较慢;
2.CoreText在渲染前就可以精确的获取展示区域的高度(只要有了CTFrame即可);UIWebView只有在加载完成之后才能知道内容的高度(且得利用JavaScript);
3.CoreText的CTFrame可以在子线程渲染;UIWebView只能在主线程渲染;
4.CoreText渲染出得内容不能像UIWebView那样方便的支持内容的复制;
5.基于CoreText来排版,需要自己处理很多复杂逻辑,包括图文混排相关逻辑,点击的操作。

相关概念

CGMutablePathRef --- CoreGraphics 下的CGPath。CGPath CGMutablePath 都定义了画path的方法,要画一个Quartz Path 到一个Context:需要通过方法 CGContextAddPath 添加path 到 graphics context,然后调用context的drawing(画图)方法。

CTFrameSetterRef --- CTFramesetter 类型用于生成text frames,CTFramesetter是CTFrame对象的对象工厂。CTFramesetter 获取attributed类型对象和一个形状描述对象,创建line 对象填充形状。输出是一个包含了一个line数组的frame对象,frame 可以直接把自己画在graphic context。

CTFrameRef --- CoreText的frame,渲染区域;每个CTFrame对象代表着一个段落。这个frame 对象是由framesetter 对象生成的能够画整个text frame 到当前的graphic context,这个frame对象包含了多行数组,这些数组能够检索单个的渲染和字形信息。

CTLineRef --- 见下方图示;
CTRunRef --- 见下方图示;

在CTFrame内部,是有多个CTLine类组成的,每一个CTLine代表一行,每个CTLine又是由多个CTRun来组成,每一个CTRun代表一组显示风格一致的文本。我们不用手工管理CTLine和CTRun的创建过程。


CTLineRef和CTRunRef.jpg
Core Text对象运行时层级.png

framesetter 调用一个typesetter对象生成 frame, 在 frame 中放置文本, framesetter将段落样式应用到这个 frame 上, 包括对齐方式, 制表符, 行间距, 缩进和断句模式. typesetter 将属性字符串中的字符转换成字形, 并将字形填充到文本框的行中

每个 CTFrame 对象包括段落的行对象(CTLine). 每个行对象代表着一行文本. 一个 CTFrame对象可能只包含一行很长的CTLine 对象或者很多行. 在framesetting操作过程中, 会创建行对象. 这些行对象跟 frame 一样, 可以直接将自己绘制到图像上下文中.

每个行对象包含一个数组的glyph run(CTRun)对象. 一个glyph run 每一行对象都包含一系列连续不断的字形,这些字形都包含相同的属性和方向. typesetter会在从字符产生行时创建glyph run对象. 这就意味着, 一个行对象是由一个或多个glyphs run 构成. glyphs run 可以将自己绘入图像上下文中, 若非必要时,大多数客户端不需要直接跟 glyph run 打交道.

相关方法

CFArrayRef CTFrameGetLines(CTFrameRef frame) //获取包含CTLineRef的数组
void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[])//获取所有CTLineRef的原点
CFRange CTLineGetStringRange(CTLineRef line) //获取line中文字在整段文字中的Range
CFArrayRef CTLineGetGlyphRuns(CTLineRef line)//获取line中包含所有run的数组
CFRange CTRunGetStringRange(CTRunRef run)//获取run在整段文字中的Range
CFIndex CTLineGetStringIndexForPosition(CTLineRef line,CGPoint position)//获取点击处position文字在整段文字中的index
CGFloat CTLineGetOffsetForStringIndex(CTLineRef line,CFIndex charIndex,CGFloat* secondaryOffset)//获取整段文字中charIndex位置的字符相对line的原点的x值

最简单的文本渲染

- (void)drawRect:(CGRect)rect {
    // Drawing code
    
    [super drawRect:rect];
    
    //1.创建上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    //2.旋转坐坐标系(默认和UIKit坐标是相反的)
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    //创建绘制区域
    CGMutablePathRef path1 = CGPathCreateMutable();
    //将绘制区域添加到rect中
//    CGPathAddRect(path, NULL, self.bounds);
    //因为坐标系是反的 所以再上面的段落rect需要注意一下
    CGPathAddRect(path1, NULL, CGRectMake(0, self.bounds.size.height/2, self.bounds.size.width, self.bounds.size.height/2));
    
    //设置绘制内容
    NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"凯文·加内特(Kevin Garnett),1976年5月19日出生在美国南卡罗来纳,前美国职业篮球运动员,司职大前锋/中锋,绰号狼王(森林狼时期)、KG(名字缩写)、The BIG TICKET、Da Kid。"
                                     "1995年NBA选秀,凯文·加内特首轮第五顺位被明尼苏达森林狼队选中,2003-04赛季获得常规赛MVP。2007年夏季转会至波士顿凯尔特人,和雷·阿伦和保罗·皮尔斯一起形成了“凯尔特人三巨头”,2008年的总决赛中击败湖人队,获得NBA总冠军。2013年,加内特被交易至布鲁克林篮网队。2015年重回明尼苏达森林狼队。"];
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
    //第一个段落
    CTFrameRef frame1 = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]/2), path1, NULL);
    
    //开始绘制
    CTFrameDraw(frame1, context);
    
    CGMutablePathRef path2 = CGPathCreateMutable();
    CGPathAddRect(path2, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height/2));
    //第二个段落
    CTFrameRef frame2 = CTFramesetterCreateFrame(framesetter, CFRangeMake([attString length]/2, [attString length]/2), path2, NULL);
    
    CTFrameDraw(frame2, context);
    
    //释放资源
    CFRelease(framesetter);
    CFRelease(frame1);
    CFRelease(path1);
    CFRelease(frame2);
    CFRelease(path2);
}


@end
多段落效果图.jpg

注意,quartz和OSX的坐标系都是右下角,而iOS的坐标系圆点是左上角,所以在iOS中使用时需要旋转一下坐标系。

//2.旋转坐坐标系(默认和UIKit坐标是相反的)
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

步骤解释:

1.获得当前绘图上下文;
2.旋转坐标系;
3.创建绘制区域,CGMutablePath;
4.将绘制区域添加到Rect中,CGPathAddRect(path, NULL, self.bounds);
5.根据NSAttributedString绘制内容生成一个CTFramesetterRef对象;
6.分别得到两个CTFrameRef对象,段落一和段落二;
7.绘制,CTFrameDraw(CTFrameRef , CGContextRef);
8.释放用到的CGMutablePath,CTFramesetterRef,CTFrameRef对象;

这里注意,跟Quartz打交道的类基本都不支持ARC,需要手动释放。

图文混排

思路:其实对于图片的排版,CoreText本身是不支持的,但是可以在需要插入图片的地方,用一个空白字符代替,同时设置该字符的CTRunDelegate为要显示图片的宽高信息,这样生成的CTFrame对象就会在绘制时,把图片的位置预留出来,之后,在drawRect方法中调用CGContextDrawImage方法直接绘制出来进行了。

主要代码如下,完整代码请看Demo。

+ (TBZCoreTextData *)parseTemplateFile:(NSString *)path config:(TBZFrameParserConfig *)config{
    NSMutableArray *mArr = [NSMutableArray array];
    NSAttributedString *attString = [self loadTemplateFile:path config:config imageArray:mArr];
    TBZCoreTextData *data = [self parseAttributedContent:attString config:config];
    data.imageArray = mArr;
    return data;
}

//方法二:读取JSON文件内容,并且调用方法三获得从NSDcitionay到NSAttributedString的转换结果
+ (NSAttributedString *)loadTemplateFile:(NSString *)path config:(TBZFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{
    NSData *data = [NSData dataWithContentsOfFile:path];
    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
    if (data) {
        
        NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
        
        if ([array isKindOfClass:[NSArray class]]) {
            for (NSDictionary *dict in array) {
                
                NSString *type = dict[@"type"];
                //区分文本和图片
                if ([type isEqualToString:@"txt"]) {
                    
                    NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
                    [result appendAttributedString:as];
                    
                }else if ([type isEqualToString:@"img"]){
                    
                    //创建TBZCoreImageData,保存图片到imageArray数组中
                    TBZCoreImageData *imageData = [[TBZCoreImageData alloc] init];
                    //设置图片的名字字符串;
                    imageData.name = dict[@"name"];
                    //设置图片的插入位置
                    imageData.position = [result length];
                    [imageArray addObject:imageData];
                    
                    //创建空白占位符,并且设置它的CTRunDelegate信息
                    NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
                    [result appendAttributedString:as];
                }
            }
        }
    }
    return  result;
}

//接受一个NSAttributedString和一个Config参数,将NSAttributedString转换成CoreTextData返回
+ (TBZCoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(TBZFrameParserConfig *)config{
    
    //创建CTFrameStterRef实例
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
    
    //获得要绘制的区域的高度
    CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, [content length]), nil, restrictSize, nil);
    CGFloat textHeight = coreTextSize.height;
    
    //生成CTFrameRef实例
    CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
    
    //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
    TBZCoreTextData *data = [[TBZCoreTextData alloc] init];
    data.ctFrame = frame;
    data.height = textHeight;
    
    //释放内存
    CFRelease(framesetter);
    CFRelease(frame);
    
    return data;
}

#pragma mark - 添加设置CTRunDelegate信息的方法
static CGFloat ascentCallback(void *ref){
    
    return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
}
static CGFloat descentCallback(void *ref){
    
    return 0;
}
static CGFloat widthCallback(void *ref){
    
    return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
}
+ (NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(TBZFrameParserConfig *)config{
    
    CTRunDelegateCallbacks callbacks;
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getDescent = descentCallback;
    callbacks.getWidth = widthCallback;
    //将宽高信息通过delegate返回
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
    
    //使用0xFFFC作为空白占位符
    unichar objectReplacementChar = 0xFFFC;
    NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
    NSDictionary *attributes = [self attributesWithConfig:config];
    NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
    //将CTRunDelegate对象跟CTAttributedString绑定
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    CFRelease(delegate);
    return space;
}


//TBZCoreTextData.m

-(void)setImageArray:(NSArray *)imageArray{
    _imageArray = imageArray;
    [self fillImagePosition];
    
}
//填充图片
-(void)fillImagePosition{
    if (self.imageArray.count==0) {
        return;
    }
    NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
    NSInteger lineCount = [lines count];
    CGPoint lineOrigins[lineCount];
    CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
    
    int imgIndex = 0;
    TBZCoreImageData *imageData = self.imageArray[0];
    for (int i=0; i<lineCount; i++) {
        if (imageData==nil) {
            break;
        }
        //获得line对象
        CTLineRef line = (__bridge CTLineRef)lines[i];
        NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
        //遍历该line对象中的run对象
        for (id runObj in runObjArray) {
            CTRunRef run = (__bridge CTRunRef)runObj;
            NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
            //如果该run对象没有CTRunDelegate对象,则结束本次循环,继续下一次循环
            if (delegate == nil) {
                continue;
            }
            //得到CTRunDelegate对象绑定的数据,CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
            NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
            //验证数据的格式
            if (![metaDic isKindOfClass:[NSDictionary class]]) {
                continue;
            }
            
            //计算图片的rect
            CGRect runBounds;
            CGFloat ascent;
            CGFloat descent;
            runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            runBounds.size.height = ascent + descent;
            
            CGFloat x0ffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            runBounds.origin.x = lineOrigins[i].x + x0ffset;
            runBounds.origin.y = lineOrigins[i].y;
            runBounds.origin.y -= descent;
            
            CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
            CGRect colRect = CGPathGetBoundingBox(pathRef);
            CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
            
            imageData.imagePostion = delegateBounds;
            imgIndex ++;
            if (imgIndex == self.imageArray.count) {
                //所有图片都处理完 结束遍历
                imageData = nil;
                break;
            }else{
                imageData = self.imageArray[imgIndex];
            }
        }
    }
}

添加对图片的点击支持

需要给展示view添加单击手势,判断手势触摸点是否在图片中,实现该功能。

代码如下:

- (instancetype)initWithFrame:(CGRect)frame{
    if ([super initWithFrame:frame]) {
        [self addGesture];
    }
    return self;
}

- (void)addGesture{
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureRecognizer:)];
    [self addGestureRecognizer:tap];
    self.userInteractionEnabled = YES;
}

- (void)tapGestureRecognizer:(UITapGestureRecognizer *)recognizer{
    CGPoint point = [recognizer locationInView:self];
    
    for (TBZCoreImageData *data in self.textData.imageArray) {
        //翻转坐标系,因为ImageData中的坐标是CoreText的坐标系
        CGRect imageRect = data.imagePostion;
        CGPoint imagePosition = imageRect.origin;
        imagePosition.y = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
        CGRect rect = CGRectMake(imagePosition.x, imagePosition.y, imageRect.size.width, imageRect.size.height);
        
        //检测点击位置Point是否在rect之内
        if (CGRectContainsPoint(rect, point)) {
            //在这里处理点击后的逻辑
            [self showTapImage:data];
            break;
        }
    }
}

- (void)showTapImage:(TBZCoreImageData *)data{
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    
    //图片
    tapImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:data.name]];
    tapImageView.frame = CGRectMake(0, 0, data.imagePostion.size.width, data.imagePostion.size.height);
    tapImageView.center = keyWindow.center;
    
    
    //蒙版
    coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
    [coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]];
    coverView.backgroundColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.6];
    coverView.userInteractionEnabled = YES;
    
    [keyWindow addSubview:coverView];
    [keyWindow addSubview:tapImageView];
}

- (void)cancel{
    [tapImageView removeFromSuperview];
    [coverView removeFromSuperview];
}

还是得注意坐标系是反的,很关键。

排版中增加链接格式,实现点击链接title,打开网页

思路:将链接的title渲染到frame中,当触摸点在这个title的range中时,拿到对应的url打开。

主要代码如下:

+ (NSAttributedString *)loadTemplateFile:(NSString *)path config:(TBZFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray linkArray:(NSMutableArray *)linkArray{
    NSData *data = [NSData dataWithContentsOfFile:path];
    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
    if (data) {
        
        NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
        
        if ([array isKindOfClass:[NSArray class]]) {
            for (NSDictionary *dict in array) {
                
                NSString *type = dict[@"type"];
                //区分文本和图片
                if ([type isEqualToString:@"txt"]) {
                    
                    NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
                    [result appendAttributedString:as];
                    
                }else if ([type isEqualToString:@"img"]){
                    
                    //创建TBZCoreImageData,保存图片到imageArray数组中
                    TBZCoreImageData *imageData = [[TBZCoreImageData alloc] init];
                    //设置图片的名字字符串;
                    imageData.name = dict[@"name"];
                    //设置图片的插入位置
                    imageData.position = [result length];
                    [imageArray addObject:imageData];
                    
                    //创建空白占位符,并且设置它的CTRunDelegate信息
                    NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
                    [result appendAttributedString:as];
                }else if ([type isEqualToString:@"link"]){
                    
                    NSUInteger startPo = [result length];
                    NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
                    [result appendAttributedString:as];
                    
                    NSRange linkRange = NSMakeRange(startPo, result.length - startPo);
                    
                    TBZCoreUrlData *urlData = [[TBZCoreUrlData alloc] init];
                    urlData.title = dict[@"content"];
                    urlData.url = dict[@"url"];
                    urlData.range = linkRange;
                    [linkArray addObject:urlData];
                }
            }
        }
    }
    return  result;
}

///TBZUrlMixedView.m
//检测点击位置是否在链接上
- (TBZCoreUrlData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(TBZCoreTextData *)data{
    
    CTFrameRef textFrame = data.ctFrame;
    CFArrayRef lines = CTFrameGetLines(textFrame);
    if (!lines) return nil;
    CFIndex count = CFArrayGetCount(lines);
    TBZCoreUrlData *foundLink = nil;
    
    //获得每一行的origin坐标
    CGPoint origins[count];
    CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), origins);
    
    //翻转坐标系
    CGAffineTransform tranform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
    tranform = CGAffineTransformScale(tranform, 1.f, -1.f);
    for (int i=0; i<count; i++) {
        CGPoint linePoint = origins[i];
        CTLineRef line = CFArrayGetValueAtIndex(lines, i);
        
        //获取每一行的CGRect信息
        CGRect flippedRect = [self getLineBounds:line point:linePoint];
        CGRect rect = CGRectApplyAffineTransform(flippedRect, tranform);
        
        if (CGRectContainsPoint(rect, point)) {
            //将点击的坐标转换成相对于当前行的坐标
            CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect), point.y-CGRectGetMinY(rect));
            
            //获得当前点击坐标对应的字符串偏移
            CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
            
            //判断这个偏移是否在我们的链接列表中
            foundLink = [self linkAtIndex:idx linkArray:data.linkArray];
            
            return foundLink;
        }
    }
    return nil;
}

//获取每一行的CGRect信息
- (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point{
    CGFloat ascent = 0.0f;
    CGFloat descent = 0.0f;
    CGFloat leading = 0.0f;
    CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
    CGFloat height = ascent + descent;
    return CGRectMake(point.x, point.y, width, height);
}

//判断这个偏移是否在我们的链接列表中
- (TBZCoreUrlData *)linkAtIndex:(CFIndex)i linkArray:(NSArray *)linkArray{
    
    TBZCoreUrlData *link = nil;
    for (TBZCoreUrlData *data in linkArray) {
        if (NSLocationInRange(i, data.range)) {
            link = data;
            break;
        }
    }
    return link;
}

Demo下载


觉得有用,请帮忙点亮红心


Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

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