使用CAGradientLayer实现颜色渐变折线图

CAGradientLayer是CALayer的一个子类,包含在QuartzCore框架中,支持两种或更多颜色平滑渐变,其绘制使用了硬件加速。本章我们介绍下如何使用CAGradientLayer绘制渐变折线图。

属性说明

CAGradientLayer共有5个主要属性:

/* 
CGColorRef对象数组,定义了每个渐变区间的渐变色 
*/
@property(nullable, copy) NSArray *colors;

/* 
NSNumber对象的可选数组定义了[0,1]范围内的每个渐变停止的位置,值必须是单调增加的。
如果给出一个nil数组,则渐变在[0,1]范围内均匀分布。
渲染时,颜色在插值之前映射到输出空间。 默认为nil。
*/
@property(nullable, copy) NSArray<NSNumber *> *locations;

/*
绘制到图层坐标空间时渐变的起点和终点。
起点对应于第一个梯度停止,终点对应于最后一个梯度停止。
两个点都在单位坐标空间中定义,然后在绘制时将其映射到图层的边界矩形。
边界矩形即[0,0]是左下角图层的一角,[1,1]是右上角。
默认值分别是[.5,0]和[.5,1]。 
*/
@property CGPoint startPoint;
@property CGPoint endPoint;

/*
渐变绘制类型,可设置`axial' (默认值), `radial', and `conic'.
*/
@property(copy) CAGradientLayerType type;

基础渐变

实现颜色渐变非常简单,只需设置colorsstartPointendPoint属性即可。
startPointendPoint决定了渐变的方向,这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标是{1, 1}

下面是实现紫红和淡红对角线渐变代码:

@interface ViewController ()
@property (nonatomic, strong) UIView *gradientContentView;
@property (nonatomic, strong) CAGradientLayer *gradientLayer;
@end

@implementation ViewController

- (void)makeConstraints
{
    __weak typeof(self) weakSelf = self;
    _gradientContentView = [[UIView alloc]init];
    [self.view addSubview:_gradientContentView];
    [self.gradientContentView mas_makeConstraints:^(MASConstraintMaker *make) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        make.centerX.equalTo(strongSelf.view.mas_centerX);
        make.centerY.equalTo(strongSelf.view.mas_centerY);
        make.width.equalTo(@(240));
        make.height.equalTo(@(300));
    }];
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    _gradientLayer.frame = self.gradientContentView.bounds;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self makeConstraints];
    
    _gradientLayer = [CAGradientLayer layer];
    // 设置渐变颜色值
    _gradientLayer.colors = @[(__bridge id)[UIColor colorWithHexString:@"#FF4275"].CGColor,
                              (__bridge id)[UIColor colorWithHexString:@"#FDF4FA"].CGColor];
    // 设置开始位置和结束点
    _gradientLayer.startPoint = CGPointMake(0, 0);
    _gradientLayer.endPoint = CGPointMake(1, 1);
    [self.gradientContentView.layer addSublayer:_gradientLayer];
}


@end

效果如图:

均匀渐变

修改渐变位置:

// 设置渐变分割位置
_gradientLayer.locations = @[@0.0, @0.35,@1.0];

效果如图:

非均匀渐变

如果你愿意,colors属性可以包含很多颜色,所以创建一个彩虹一样的多重渐变也是很简单的。默认情况下,这些颜色在空间上均匀地被渲染,但是我们可以用locations属性来调整空间。locations属性是一个浮点数值的数组(以NSNumber包装)。这些浮点数定义了colors属性中每个不同颜色的位置,同样的,也是以单位坐标系进行标定。0.0代表着渐变的开始,1.0代表着结束。
locations数组并不是强制要求的,但是如果你给它赋值了就一定要确保locations的数组大小和colors数组大小一定要相同,否则你将会得到一个空白的渐变。

目标动画效果

熟悉CAGradientLayer的基础用法后,我们结合其他Layer实现下图动画展示的复杂渐变折线:

渐变折线图.gif

如图:Y轴是数值,X轴是日期,每个日期值对应一个小红点记录(红点支持点击),小红点连成折线,折线以下区域根据Y轴值域进行渐变,从透明到淡紫红色,折线图在记录较多时支持左右滑动

准备JSON数据

下面是一组服务器的肤质记录数据,包含测肤时间、照片分数、测试记录id。

{
    avgScore = 83;
    list =     (
                {
            measureTime = "2019-03-25 17:46:46";
            photoScore = 64;
            testHistoryId = 1594780;
        },
                {
            measureTime = "2019-01-25 15:01:13";
            photoScore = 89;
            testHistoryId = 1359786;
        },
                {
            measureTime = "2019-01-24 16:34:30";
            photoScore = 90;
            testHistoryId = 1355844;
        },
                {
            measureTime = "2019-01-02 15:18:56";
            photoScore = 74;
            testHistoryId = 1271310;
        },
                {
            measureTime = "2018-12-29 15:00:02";
            photoScore = 80;
            testHistoryId = 1255037;
        },
                {
            measureTime = "2018-12-28 09:50:38";
            photoScore = 78;
            testHistoryId = 1250126;
        },
                {
            measureTime = "2018-12-27 16:54:13";
            photoScore = 85;
            testHistoryId = 1248122;
        },
                {
            measureTime = "2018-12-26 17:53:59";
            photoScore = 84;
            testHistoryId = 1244881;
        },
                {
            measureTime = "2018-11-07 09:45:26";
            photoScore = 80;
            testHistoryId = 1033831;
        },
                {
            measureTime = "2018-10-30 16:48:12";
            photoScore = 82;
            testHistoryId = 1000827;
        },
                {
            measureTime = "2018-10-29 19:42:25";
            photoScore = 76;
            testHistoryId = 996757;
        },
                {
            measureTime = "2018-08-21 17:49:42";
            photoScore = 88;
            testHistoryId = 690325;
        },
                {
            measureTime = "2018-08-17 14:38:39";
            photoScore = 90;
            testHistoryId = 672420;
        },
                {
            measureTime = "2018-08-16 20:03:22";
            photoScore = 89;
            testHistoryId = 669666;
        },
                {
            measureTime = "2018-08-10 20:47:43";
            photoScore = 90;
            testHistoryId = 642235;
        },
                {
            measureTime = "2018-07-19 17:27:44";
            photoScore = 88;
            testHistoryId = 537337;
        },
                {
            measureTime = "2018-07-18 16:43:24";
            photoScore = 89;
            testHistoryId = 533668;
        },
                {
            measureTime = "2018-07-17 21:03:19";
            photoScore = 92;
            testHistoryId = 530970;
        }
    );
    maxScore = 92;
}

定义数据模型

1、定义数据模型

针对以上JSON数据,定义数据模型MGLDRecordModel如下:

@interface MGLDRecordModel : NSObject
@property (nonatomic, strong) NSString *testHistoryId;
@property (nonatomic, strong) NSNumber *photoScore;
@property (nonatomic, strong) NSString *measureTime;
// 按照图表 `月/日` 进行时间格式化
- (NSString*)formattedTime;
@end

@implementation MGLDRecordModel
- (NSString*)formattedTime
{
    if (self.measureTime) {
        return nil;
    }
    NSDate *date = [NSDate dateWithString:self.measureTime formatString:@"yyyy-MM-dd HH:mm:ss"];
    return [NSString stringWithFormat:@"%@/%@",@(date.month),@(date.day)];
}
@end

2、JSON-Model序列化

提取上述JSONlist数组并进行对象序列化

NSDictionary *jsonObject = [self getResponseJSON];
if (jsonObject && [jsonObject.allKeys containsObject:@"list"]) 
{
    NSArray *list = jsonObject[@"list"];
        
    NSMutableArray *tmpArray = [[NSMutableArray alloc]init];
    for (int index = 0; index < list.count; index++) {
       NSDictionary *param = list[index];
         MGLDRecordModel *aModel = [MGLDRecordModel modelWithJSON:param];
        if (aModel) {
             [tmpArray addObject:aModel];
         }
    }
        // 绘图
}

设计图层结构

从目标动画gif图可以看出,折线图的宽度大于可见区域,不能一次性全部显示,X轴的日期跟随折线图移动,Y轴和背景虚实线保持静止,可拆解主要组成元素为以下三部分:

  • 底部背景视图(MGLDHistoryGraphView类)

背景图层主要用于显示Y坐标、虚实线,并容纳滚动容器,本例中Y坐标使用UILabel标签布局组成,虚实线采用Draw方法绘制.

  • 中间滚动容器(UIScrollView类)

中间滚动容器采用UIScrollView,边界为父容器的Y轴右边边界区域,contentSize与画板size保持一直

  • 核心画板(MGLDDrawingBoard类)

画板用于绘制渐变区域、渐变折线,容纳交互按钮、X轴日期标签等,画板的宽度根据记录的数量动态计算

视图类自底而上依次为MGLDHistoryGraphView->UIScrollView->MGLDDrawingBoard

图层结构如下:

图层结构

开始绘图

1、绘制底层背景

按照上述图层设计,我们从下往上先绘制底层的背景如下:


底层坐标轴

如图,最底层视图主要由Y坐标、虚线、实线和白色背景组成,我们依次绘制:

1.1、绘制Y坐标轴

- (void)drawYAxisValuesInRect:(CGRect)rect
{
    CGFloat height = self.bounds.size.height;
    // static CGFloat KTopSpaceValue = 20.0f; static CGFloat KBottomSpaceValue = 34.0f;
    CGFloat drawContainerHeight = height - KTopSpaceValue - KBottomSpaceValue;
    CGFloat dashLinePadding = drawContainerHeight / 5.0;
    
    NSArray *yAxisValue = @[@"100",@"80",@"60",@"40",@"20"];
    NSUInteger count = yAxisValue.count;
    for (int index = 0; index < count; index++)
    {
        CGFloat originY = KTopSpaceValue + index*dashLinePadding - 9;
        UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(0, originY, KYAxisLeftSpaceValue-5, 18)];
        label.textAlignment = NSTextAlignmentRight;
        label.backgroundColor = [UIColor clearColor];
        label.font = [UIFont systemFontOfSize:12];
        label.textColor = [UIColor colorWithWhite:0.5  alpha:1.0];
        label.text = yAxisValue[index];
        [self addSubview:label];
    }
}

1.2、绘制辅助虚线

- (void)drawAuxiliaryDashedLineInRect:(CGRect)rect
{
    CGFloat height = self.bounds.size.height;
    CGFloat width = self.bounds.size.width;
    CGFloat drawContainerHeight = height - KTopSpaceValue - KBottomSpaceValue;
    CGFloat dashLinePadding = drawContainerHeight / 5.0;
    
    [[UIColor colorWithHexString:@"#E0E1E7"]setStroke];
    UIBezierPath *path = [UIBezierPath bezierPath];
    path.lineWidth = 1.0;
    CGFloat dash[] = {6,5};
    [path setLineDash:dash count:2 phase:0];
    for (int index = 0; index < 5; index ++) {
        [path moveToPoint:CGPointMake(KYAxisLeftSpaceValue, KTopSpaceValue + index*dashLinePadding)];
        [path addLineToPoint:CGPointMake(width, KTopSpaceValue + index*dashLinePadding)];
    }
    [path stroke];
}

1.3、绘制底部实线

- (void)drawBottomLineInRect:(CGRect)rect
{
    CGFloat height = self.bounds.size.height;
    CGFloat width = self.bounds.size.width;
    
    UIBezierPath *path = [UIBezierPath bezierPath];
    
    [path moveToPoint:CGPointMake(KYAxisLeftSpaceValue, height - KBottomSpaceValue)];
    [path addLineToPoint:CGPointMake(width, height - KBottomSpaceValue)];
    
    [[UIColor colorWithHexString:@"#E0E1E7"]setStroke];
    path.lineWidth = 1.0;
    [path stroke];
}

2、添加UIScrollView

添加UIScrollView,保持top、bottom、right与父视图一致,左边从Y坐标轴右边区域开始,背景透明,为了便于观察,我们添加#FF4275背景色

添加UIScrollView

布局如下:

@weakify(self);
_scrollView = [[UIScrollView alloc]init];
// 设置紫红背景色便于观察
_scrollView.backgroundColor = [UIColor colorWithHexString:@"#FF4275"];
_scrollView.showsVerticalScrollIndicator = NO;
_scrollView.showsHorizontalScrollIndicator = NO;
[self addSubview:_scrollView];
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
    @strongify(self);
    make.left.equalTo(self).offset(KYAxisLeftSpaceValue);
    make.right.top.bottom.equalTo(self);
}];

3、添加画板

画板是整个折线图的核心部分,用来绘制渐变色,X轴坐标、折线、交互按钮。

添加MGLDDrawingBoard画板到UIScrollView,设置边缘与UIScrollView保持一直,高度等于UIScrollView高度,宽度根据折线图绘制的宽度计算。

_drawingBoard = [[MGLDDrawingBoard alloc]init];
// 设置橙色背景色便于观察
_drawingBoard.backgroundColor = [UIColor orangeColor];   
[self.scrollView addSubview:_drawingBoard];
[self.drawingBoard mas_makeConstraints:^(MASConstraintMaker *make) {
    @strongify(self);
    make.edges.equalTo(self.scrollView);
    make.height.equalTo(self.scrollView.mas_height);
    // 初始默认宽度,绘制曲线时需要更新
    make.width.equalTo(@(10));
}];

效果如图:

画板布局图

3.1、背景图层接口配置

背景图层接口调用进行绘制更新时,需要根据记录个数调整画板的宽度,记录过少时在画板绘制区间均匀分布,记录过多时固定宽度分布,左右滚动查看

/**
 折线图容器,包含UIScrollView、画板以及其他元素
 */
@interface MGLDHistoryGraphView : UIView
- (void)drawGraphWithValues:(NSArray<MGLDRecordModel*>*)values;
@end

@implementation MGLDHistoryGraphView
- (void)drawGraphWithValues:(NSArray<MGLDRecordModel *> *)values
{
    NSUInteger count = values.count;
    self.itemCount = count;
    [self setNeedsLayout];
    // 绘制渐变折线图
    [self.drawingBoard drawGraphWithValues:values];
}

/*
 折线图在记录过少需要在可见屏幕均匀分布,记录过多左右滚动
 我们需要在此函数调用时刷新画板的边界
*/
- (void)layoutSubviews
{
    [super layoutSubviews];
    CGFloat width = CGRectGetWidth(self.bounds);
    // 如果可绘制的记录总宽度小于当前容器的宽度,则让记录在容器空间均匀分布
    if (self.itemCount*KItemSpaceValue < width)
    {
        [self.drawingBoard mas_updateConstraints:^(MASConstraintMaker *make) {
            make.width.equalTo(@(width));
        }];
    }
    else
    {
        CGFloat drawingBoardWidth = (self.itemCount+2)*KItemSpaceValue;
        [self.drawingBoard mas_updateConstraints:^(MASConstraintMaker *make) {
            make.width.equalTo(@(drawingBoardWidth));
        }];
    }
}
@end

3.2、画板接口配置

在画板页面,我们将交互按钮和X轴坐标分别用UIButton和UILabel进行展示,这里预先缓存控件用于后面的layout刷新。

/**
 画板,用于折线图内容绘制
 */
@interface MGLDDrawingBoard : UIView
- (void)drawGraphWithValues:(NSArray<MGLDRecordModel*>*)values;
@end

- (void)drawGraphWithValues:(NSArray<MGLDRecordModel *> *)values
{
// 加锁,防止重新绘制和layout布局同时调用造成资源访问冲突
    [_lock lock];
    self.drawValues = values;
    if (values.count > 0) {
         // 默认选中最后一个
        MGLDRecordModel *aModel = [values lastObject];
        self.selectedRecordId = aModel.testHistoryId;
    }
    // 移除按钮和标签
    [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
    
    // 缓存画板上的按钮和标签
    NSMutableArray *buttonsArray = [NSMutableArray array];
    NSMutableArray *labelsArray = [NSMutableArray array];
    for (int index = 0; index < values.count; index++) {
        MGLDRecordModel *aModel = [values objectAtIndex:index];
        UIButton *button = [self buttonWithRecordId:aModel.testHistoryId tag:index];
        [self addSubview:button];
        [buttonsArray addObject:button];
        
        UILabel *label = [self labelWithText:[aModel formattedTime]];
        [self addSubview:label];
        [labelsArray addObject:label];
    }
    self.buttons = buttonsArray;
    self.labels = labelsArray;
    [_lock unlock];
    // 触发layout布局
    [self setNeedsLayout];
}

3.3、设置画板渐变和折线图层

@interface MGLDDrawingBoard ()
@property (nonatomic, strong) CAGradientLayer *gradientLayer;
@property (nonatomic, strong) CAShapeLayer *lineLayer;
@property (nonatomic, strong) NSArray<MGLDRecordModel *> *drawValues;
@property (nonatomic, strong) NSArray *buttons;
@property (nonatomic, strong) NSArray *labels;
@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, strong) NSNumber *selectedRecordId;
@end

@implementation MGLDDrawingBoard
- (void)setUp
{
    self.backgroundColor = [UIColor clearColor];
    _lock = [[NSLock alloc]init];
    // 渐变图层
    _gradientLayer = [CAGradientLayer layer];
    NSArray *colors = @[(__bridge id)[UIColor colorWithHexString:@"#FF4275"].CGColor,
                        (__bridge id)[UIColor colorWithHexString:@"#FFC7D6"].CGColor,
                        (__bridge id)[UIColor colorWithHexString:@"#FFE4EC"].CGColor,
                        (__bridge id)[UIColor colorWithHexString:@"#FFF5F7"].CGColor,
                        (__bridge id)[UIColor whiteColor].CGColor];
    _gradientLayer.colors = colors;
    _gradientLayer.locations = @[@0.0,@0.4,@0.6,@0.8,@1.0];
    _gradientLayer.startPoint = CGPointMake(0.5,0.0);
    _gradientLayer.endPoint = CGPointMake(0.5,1.0);
    _gradientLayer.opacity = 0.5;
    [self.layer addSublayer:_gradientLayer];
    
    // 折线图层
    _lineLayer = [CAShapeLayer layer];
    _lineLayer.allowsEdgeAntialiasing = YES;
    _lineLayer.strokeColor = [UIColor colorWithHexString:@"#FF4275"].CGColor;
    _lineLayer.fillColor = [UIColor clearColor].CGColor;
    _lineLayer.lineWidth = 2.0;
    _lineLayer.lineJoin = kCALineJoinRound;
    _lineLayer.lineCap = kCALineCapRound;
    [self.layer addSublayer:_lineLayer];
}
@end

3.4、图层绘制和按钮标签布局

这里将layer路径设置和按钮、标签坐标更新放到一起计算

- (void)layoutSubviews
{
    [super layoutSubviews];
    // 调整layer的框架
    _lineLayer.frame = self.bounds;
    _gradientLayer.frame = self.bounds;
    
    [_lock lock];
    if (self.drawValues.count <= 0) {
        [_lock unlock];
        return;
    }
    
    CGFloat width = CGRectGetWidth(self.bounds);
    CGFloat height = CGRectGetHeight(self.bounds);
    CGFloat drawSpaceHeight = height - KTopSpaceValue - KBottomSpaceValue;
    
    
    MGLDRecordModel *firstModel = [self.drawValues firstObject];
    MGLDRecordModel *lastModel = [self.drawValues lastObject];
    
    CGFloat firstPointY = [self getOriginY:firstModel.photoScore.floatValue spaceHeight:drawSpaceHeight];
    CGFloat lastPointY = [self getOriginY:lastModel.photoScore.floatValue spaceHeight:drawSpaceHeight];
    
    UIBezierPath *gradientPath = [UIBezierPath bezierPath];
    UIBezierPath *linePath = [UIBezierPath bezierPath];
    [gradientPath moveToPoint:CGPointMake(-1, firstPointY)];
    [linePath moveToPoint:CGPointMake(0, firstPointY)];
    
    NSUInteger count = self.drawValues.count;
    CGFloat superWidth =  CGRectGetWidth(self.superview.frame);
    if (count*KItemSpaceValue < superWidth)
    {
        CGFloat padding = width/(count + 1);
        CGFloat originX = (width - MAX(0, count-1)*padding)/2;
        for (int index = 0; index < count; index++)
        {
            MGLDRecordModel *aModel = [self.drawValues objectAtIndex:index];
            CGFloat pointY = [self getOriginY:aModel.photoScore.floatValue spaceHeight:drawSpaceHeight];
            // 添加路径
            CGPoint pathPoint = CGPointMake(index*padding + originX, pointY);
            [gradientPath addLineToPoint:pathPoint];
            [linePath addLineToPoint:pathPoint];
            
            // 更新按钮和标签的center
            if (index < self.buttons.count) {
                UIButton *button = [self.buttons objectAtIndex:index];
                button.center = pathPoint;
            }
            CGPoint labelPoint = CGPointMake(index * padding + originX, height - KBottomSpaceValue*0.5);
            if (index < self.labels.count) {
                UILabel *label = [self.labels objectAtIndex:index];
                label.center = labelPoint;
            }
        }
    }
    else
    {
        for (int index = 0; index < count; index++)
        {
            MGLDRecordModel *aModel = [self.drawValues objectAtIndex:index];
            CGFloat pointY = [self getOriginY:aModel.photoScore.floatValue spaceHeight:drawSpaceHeight];
            CGPoint pathPoint = CGPointMake((index + 1)*KItemSpaceValue, pointY);
            [gradientPath addLineToPoint:pathPoint];
            [linePath addLineToPoint:pathPoint];
            if (index < self.buttons.count) {
                UIButton *button = [self.buttons objectAtIndex:index];
                button.center = pathPoint;
            }
            CGPoint labelPoint = CGPointMake((index + 1)*KItemSpaceValue, height - KBottomSpaceValue*0.5);
            if (index < self.labels.count) {
                UILabel *label = [self.labels objectAtIndex:index];
                [label sizeToFit];
                label.center = labelPoint;
            }
        }
    }
    
    [gradientPath addLineToPoint:CGPointMake(width+1, lastPointY)];
    [linePath addLineToPoint:CGPointMake(width, lastPointY)];
    
    // 构造渐变layer闭环
    [gradientPath addLineToPoint:CGPointMake(width+1, height - KBottomSpaceValue)];
    [gradientPath addLineToPoint:CGPointMake(-1, height - KBottomSpaceValue)];
    [gradientPath addLineToPoint:CGPointMake(-1, firstPointY)];
    
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = gradientPath.CGPath;
    _gradientLayer.mask = shapeLayer;
    _lineLayer.path = linePath.CGPath;
    
    // 设置选中状态,第一次排版时刷新布局并滚动到对应位置
    for (UIButton *button in self.buttons) {
        if (self.selectedRecordId && [button.recordId isEqualToNumber:self.selectedRecordId]) {
            button.selected = YES;
            CGFloat originX = button.frame.origin.x;
            UIScrollView *scrollView = (UIScrollView*)self.superview;
            if (scrollView.frame.size.width < scrollView.contentSize.width) {
                CGFloat width = scrollView.frame.size.width;
                CGFloat offsetX = MAX(0, originX + KItemSpaceValue-width);
                [(UIScrollView*)self.superview setContentOffset:CGPointMake(offsetX, 0) animated:YES];
            }
        }
        else
        {
            button.selected = NO;
        }
    }
    
    [_lock unlock];
}

最终完成效果同上:


渐变折线图.gif

注:实际上画板上的渐变、折线、按钮和标签都是相互独立的部分,经过组合得到图示效果。

源代码

附上Demo源代码

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

推荐阅读更多精彩内容

  • 项目中要使用有渐变颜色的折线图,所以最近研究了一下,写一个简单的教程,主要是为了能让人明白最主要的功能,主要讲的是...
    静谧的橘子阅读 1,380评论 0 1
  • 项目中要使用有渐变颜色的折线图,所以最近研究了一下,写一个简单的教程,主要是为了能让人明白最主要的功能,主要讲的是...
    静谧的桔子阅读 1,196评论 0 2
  • 涉及知识点:CGContextRef // 联系图形上下文,可以说是画布的存在吧UIBezierPath // 贝...
    little曾阅读 631评论 2 7
  • 一个折线图的需求,好久没写过折线图了,打算自己弄下。对于折线图或者条形统计图来说,我很少自己去封装,并不是有多难有...
    举个栗子wow阅读 2,371评论 1 3
  • 为自己而活着还是为别人而活着?多少人思考着了多少年的问题了 人是为别人而活着,还是为自己而活着的?在我的生活中我明...
    心若向阳_阅读 124评论 1 1