一步步实现Uber/优步启动动画

原作者:Derek Selander 2016/08/01 原文
翻译: yuan 2016/08/30
iOS Uber/优步开屏动画 Swift

通常,App启动时会需要一些必要的初始化数据。所以App会向服务器发起请求并等待这些数据的返回。这时,一个完美的开屏动画,可以让用户在等待时保持一个愉悦的心情。

16年的上半年,Uber决定重新设立品牌形象与Logo。其中就包含了一个重新设计的炫酷的开屏动画。

这篇文章将会尝试复现一个与Uber开屏动画非常相近的效果。其中会用到CALayersCAAnimations相关一些类。这篇文章将会更侧重实现,其中包含的一些动画的概念,可以在点击订阅 Marin Todorov’s Intermediate iOS Animation video series.来了解详情。

初始

在这篇文章中你将会用到非常多的动画,你可能需要一个已经创建好CALayer起始项目

这个项目被命名为Fuber,Fuber是一个共享出行理念的App。用户可以发起一个平衡车分享需求给平衡车司机,同时要求司机把你从一个地方送到另外一个地方。Fuber正在快速的增长并且已经渗入了60多个国家。 但Fuber遭遇到了一些当地政府与平衡车工会的抵制。

在这篇文章的结束, 你将会实现一个这样的开屏动画:

尝试运行的起始项目

项目最开始会调起RootContainerViewController。同时RootContainerViewController调起子控制器SplashViewControllerRootContainerViewController会一直展示开屏动画直到App已经准备好启动。这就像我们的App在与服务器进行数据请求时的交互一样。在这次的项目里,开屏动画将会有自己的结束逻辑。

RootContainerViewController主要实现了两个方法 showSplashViewController()showSplashViewControllerNoPing()。为了方便观察,我们将会使用showSplashViewControllerNoPing()来创建一个无限循环的开屏动画。在完成这篇教程后,你可以尝试使用showSplashViewController()来模拟真实的API请求。

开屏动画视图与图层结构

SplashViewController的视图包含了两个子视图。第一个子视图包含了一个TileGridView波浪形网视图的背景。这个网状视图其实是一个网格式的布局,类似UICollectionView,每一个网格里包含了一个TileView。另外一个子视图是AnimatedULogoView,一个‘U'形的动画层。

AnimatedULogoView包含了4个CAShapeLayer:

  • circleLayer代表的是‘U’形的圆形的背景

  • lineLayer是一条直线,从circleLayer的中间延伸到边缘

  • squareLayer一个位于circleLayer中间的正方形

  • maskLayer用于遮盖子视图。用来组合其他视图,简单的实现一个遮盖子视图的效果

这些图层组合起来, 将会是Fuber的‘U’

现在你已经知道你所需要的组合视图结构,可以去尝试让他们动起来了。

圆圈动画

为了方便观察动画的效果,我们可以把其他不需要的图层注释掉。在AnimatedULogoView.swift文件里找到init(frame:)方法,把除了circleLayer的其他的三个图层全部注释掉。之后我们再慢慢一步一步把他们还原回来。代码:


override init(frame: CGRect) {

super.init(frame: frame)

circleLayer = generateCircleLayer()

lineLayer = generateLineLayer()

squareLayer = generateSquareLayer()

maskLayer = generateMaskLayer()

//  layer.mask = maskLayer

layer.addSublayer(circleLayer)

//  layer.addSublayer(lineLayer)

//  layer.addSublayer(squareLayer)

}

generateCircleLayer()里创建圆形,使用了CAShapeLayer并且再创建了一个UIBezierPath


layer.path = UIBezierPath(arcCenter: CGPointZero, radius: radius/2, startAngle: -CGFloat(M_PI_2), endAngle: CGFloat(3*M_PI_2), clockwise: true).CGPath

如果你使用0为起始角度,贝塞尔曲线将会从时钟的3点位置开始。为了从时钟的12点位置开始,把起始角度为-M_PI_2,-90度。并且设置你的结束角度为3*M_PI_2,270度,再次回到时钟的12点位置。这样你就会有一个闭合圆圈。注意,这里你想连动的是stroke属性,所以你使用radius值作为lineWidth的值。

这个circleLayer的动画需要组合3个组CAAnimation:一个CAKeyframeAnimation动画关联strokeEnd、一个CABasicAnimation动画关联transformCAAnimationGroup组合前两个动画到一起。

定位到animateCircleLayer()方法,然后添加下面的代码


// strokeEnd

let strokeEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")

strokeEndAnimation.timingFunction = strokeEndTimingFunction

strokeEndAnimation.duration = kAnimationDuration - kAnimationDurationDelay

strokeEndAnimation.values = [0.0, 1.0]

strokeEndAnimation.keyTimes = [0.0, 1.0]

设置动画的values属性为0.0到1.0,标识从起始角度开始一路绘制到圆的结束角度。类似一个顺时针动画。当这个动画的关键字strokeEnd的值慢慢增加,线的宽度也会慢慢增加,这个圆就会被慢慢填充。如果这时,你把values改成从0.0到0.5,只有一个半圆会被显示出来。 这是因为绘制路径strokeEnd停止在了半程0.5

添加transform动画


// transform

let transformAnimation = CABasicAnimation(keyPath: "transform")

transformAnimation.timingFunction = strokeEndTimingFunction

transformAnimation.duration = kAnimationDuration - kAnimationDurationDelay

var startingTransform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0, 0, 1)

startingTransform = CATransform3DScale(startingTransform, 0.25, 0.25, 1)

transformAnimation.fromValue = NSValue(CATransform3D: startingTransform)

transformAnimation.toValue = NSValue(CATransform3D: CATransform3DIdentity)

这组动画包含了等比放大缩小与z轴的旋转。circleLayer图层慢慢变大,同时伴随顺时针45度的旋转。这个旋转的动画需要与lineLayer的旋转动画保持同步。

最后,添加一个CAAnimationGroup。这个动画包含了上面的两组动画,这样我们就只需要添加一个动画在circleLayer的图层上


// Group

let groupAnimation = CAAnimationGroup()

groupAnimation.animations = [strokeEndAnimation, transformAnimation]

groupAnimation.repeatCount = Float.infinity

groupAnimation.duration = kAnimationDuration

groupAnimation.beginTime = beginTime

groupAnimation.timeOffset = startTimeOffset

circleLayer.addAnimation(groupAnimation, forKey: "looping")

这个CAAnimationGroup有两个重要属性需要注意:beginTimetimeOffset。如果你对这两个属性不太熟悉,可以查看这篇文章

这个组合动画的beginTime被设置为了父视图的动画开始时间。

因为这个动画第一次运行时的成像不是从0开始的,所以timeOffset也被设置了。

你可以尝试改变startTimeOffset的值。观察改变后所带来的效果。

groupAnimation赋值给circleLayer后,运行查看效果,应该与这个一样:

提示: 你可以尝试去除中间的任何一个动画,来查看另一个动画的效果。尝试去改变他们的属性,你会有所启发。下面的所有组合动画你都可以这样去观察它们。

直线动画

现在可以尝试把lineLayer的动画添加进去了。 还是在AnimatedULogoView.swiftstartAnimating()方法里面去除animateLineLayer()的注释。同时把其他的动画注释掉。


public func startAnimating() {

beginTime = CACurrentMediaTime()

layer.anchorPoint = CGPointZero

//  animateMaskLayer()

//  animateCircleLayer()

animateLineLayer()

//  animateSquareLayer()

}

init(frame:)方法里,把里面的代码改为只有circleLayerlineLayer图层显示


override init(frame: CGRect) {

super.init(frame: frame)

circleLayer = generateCircleLayer()

lineLayer = generateLineLayer()

squareLayer = generateSquareLayer()

maskLayer = generateMaskLayer()

//  layer.mask = maskLayer

layer.addSublayer(circleLayer)

layer.addSublayer(lineLayer)

//  layer.addSublayer(squareLayer)

}

找到animateLineLayer()方法,输入下个组合动画:


// lineWidth

let lineWidthAnimation = CAKeyframeAnimation(keyPath: "lineWidth")

lineWidthAnimation.values = [0.0, 5.0, 0.0]

lineWidthAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction]

lineWidthAnimation.duration = kAnimationDuration

lineWidthAnimation.keyTimes = [0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]

这组动画会把线宽变粗再把线宽变细。

继续添加下一组动画:


// transform

let transformAnimation = CAKeyframeAnimation(keyPath: "transform")

transformAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction]

transformAnimation.duration = kAnimationDuration

transformAnimation.keyTimes = [0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]

var transform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0.0, 0.0, 1.0)

transform = CATransform3DScale(transform, 0.25, 0.25, 1.0)

transformAnimation.values = [NSValue(CATransform3D: transform),

NSValue(CATransform3D: CATransform3DIdentity),

NSValue(CATransform3D: CATransform3DMakeScale(0.15, 0.15, 1.0))]

就像circleLayer的动画一样,这里你也会在z轴上旋转他,并改变他的大小。 但是在这个视图里面, 你先把他缩小到原比例的25%,再马上把他回放到原来的尺寸。最后再把他缩小到原比例的15%大小。

把这两组动画放进CAAnimationGroup里面赋值给lineLayer:


// Group

let groupAnimation = CAAnimationGroup()

groupAnimation.repeatCount = Float.infinity

groupAnimation.removedOnCompletion = false

groupAnimation.duration = kAnimationDuration

groupAnimation.beginTime = beginTime

groupAnimation.animations = [lineWidthAnimation, transformAnimation]

groupAnimation.timeOffset = startTimeOffset

lineLayer.addAnimation(groupAnimation, forKey: "looping")

运行,查看他的效果

这里的起始旋转位置与circleLayer的起始旋转位置一致-M_PI_4。同时把关键帧时间keyTimes设置为[0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]。这个组数的起始与结束值非常明显为0.0与1.0,开始与结束。这个中间值是圆形绘制完成,转换为缩小的关键时间点。用kAnimationDurationDelay除以kAnimationDuration得到延迟的百分比。然后在用1减去这个百分比,来获取圆形图层的真实动画时间百分比。

现在可以移动到下一个正方形的动画上了。

正方形动画的实现

和上面的步奏类似,找到startAnimating()方法,注释掉除了animateSquareLayer()以外的其他动画。同时在init(frame:)里面改变代码为:


override init(frame: CGRect) {

super.init(frame: frame)

circleLayer = generateCircleLayer()

lineLayer = generateLineLayer()

squareLayer = generateSquareLayer()

maskLayer = generateMaskLayer()

//  layer.mask = maskLayer

layer.addSublayer(circleLayer)

//  layer.addSublayer(lineLayer)

layer.addSublayer(squareLayer)

}

跳到animateSquareLayer()方法,添加下面动画:


// bounds

let b1 = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: 2.0/3.0 * squareLayerLength, height: 2.0/3.0  * squareLayerLength))

let b2 = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: squareLayerLength, height: squareLayerLength))

let b3 = NSValue(CGRect: CGRectZero)

let boundsAnimation = CAKeyframeAnimation(keyPath: "bounds")

boundsAnimation.values = [b1, b2, b3]

boundsAnimation.timingFunctions = [fadeInSquareTimingFunction, squareLayerTimingFunction]

boundsAnimation.duration = kAnimationDuration

boundsAnimation.keyTimes = [0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]

这部分动画改变CALayerbounds属性。 图形的边框长度从2/3变化到1,再变化到0。

接着 改变图形的背景颜色:


// backgroundColor

let backgroundColorAnimation = CABasicAnimation(keyPath: "backgroundColor")

backgroundColorAnimation.fromValue = UIColor.whiteColor().CGColor

backgroundColorAnimation.toValue = UIColor.fuberBlue().CGColor

backgroundColorAnimation.timingFunction = squareLayerTimingFunction

backgroundColorAnimation.fillMode = kCAFillModeBoth

backgroundColorAnimation.beginTime = kAnimationDurationDelay * 2.0 / kAnimationDuration

backgroundColorAnimation.duration = kAnimationDuration / (kAnimationDuration - kAnimationDurationDelay)

注意fillMode属性。因为beginTime不是0,在动画开始与结速的时候CGColor可能会没有值。当这个动画添加到父类的组合动画时,为了不会有闪烁的不良体验,我们需要把属性赋值为kCAFillModeBoth

接着添加进组合动画


// Group

let groupAnimation = CAAnimationGroup()

groupAnimation.animations = [boundsAnimation, backgroundColorAnimation]

groupAnimation.repeatCount = Float.infinity

groupAnimation.duration = kAnimationDuration

groupAnimation.removedOnCompletion = false

groupAnimation.beginTime = beginTime

groupAnimation.timeOffset = startTimeOffset

squareLayer.addAnimation(groupAnimation, forKey: "looping")

运行并且查看效果:

注意:运行动画在模拟器上可能会有一些不一样,因为你的电脑在模拟一个iOS设备的GPU。 如果你的动画效果真的不一样,尝试切换到一个小屏幕的模拟器上,或者切换到真机上

遮挡图层

现在是时候把上面所有的动画组合起来了。去除掉所有在init(frame:)startAnimating()的注释。 然后运行:

看起来还是有一点不一样。动画有一个突然的图框bounds改变的效果,并不是渐变的,因为circleLayerbounds突然缩小到了0。 我们可以用一个masklayer来弥补这一切。一次性缩小所有子图层

找到animateMaskLayer()添加代码


// bounds

let boundsAnimation = CABasicAnimation(keyPath: "bounds")

boundsAnimation.fromValue = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: radius * 2.0, height: radius * 2))

boundsAnimation.toValue = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: 2.0/3.0 * squareLayerLength, height: 2.0/3.0 * squareLayerLength))

boundsAnimation.duration = kAnimationDurationDelay

boundsAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay

boundsAnimation.timingFunction = circleLayerTimingFunction

这些代码改变边框大小。 当边框大小改变时,整个AnimatedULogoView会慢慢消失,因为这个图层是所有子图层的遮挡图层。

给这个图层添加圆角:


// cornerRadius

let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")

cornerRadiusAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay

cornerRadiusAnimation.duration = kAnimationDurationDelay

cornerRadiusAnimation.fromValue = radius

cornerRadiusAnimation.toValue = 2

cornerRadiusAnimation.timingFunction = circleLayerTimingFunction

把动画组合在一个CAAnimationGroup里面


// Group

let groupAnimation = CAAnimationGroup()

groupAnimation.removedOnCompletion = false

groupAnimation.fillMode = kCAFillModeBoth

groupAnimation.beginTime = beginTime

groupAnimation.repeatCount = Float.infinity

groupAnimation.duration = kAnimationDuration

groupAnimation.animations = [boundsAnimation, cornerRadiusAnimation]

groupAnimation.timeOffset = startTimeOffset

maskLayer.addAnimation(groupAnimation, forKey: "looping")

再次运行:

网视图

TileGridView背景网状图包含了一系列的TileViews。在TileView.swift里面找到init(frame:)方法,然后添加下面的代码:


layer.borderWidth = 2.0

运行后查看效果:

你可以看见,TileView是被平铺像网格一样的布局在父视图上的。在TileGridView.swiftrenderTileViews()方法里面找到这个网格的创建逻辑。这个逻辑已经创建好了,我们只需要关注如何实现我们的动画。

网视图动画

TileGridView只有一个子视图叫containerView。它包含了所有的TileView。同时,还有一个属性叫titleViewRow,它是一个二维的数组包含了所有的TileView

TileViewinit(frame:)方法里,删掉掉刚刚添加的边宽代码。然后去除掉给layer.coents添加chimeSplashImage图片的注释:


override init(frame: CGRect) {

super.init(frame: frame)

layer.contents = TileView.chimesSplashImage.CGImage

layer.shouldRasterize = true

}

运行查看效果:

已经小有样子了

TileGridView与它所有的TileViews还需要一些动画。在TileView.swift中找到startAnimatingWithDuration(_:beginTime:rippleDelay:rippleOffset:)方法,然后输入下面的代码:


let timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.2, 1)

let linearFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

let easeOutFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)

let easeInOutTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

let zeroPointValue = NSValue(CGPoint: CGPointZero)

var animations = [CAAnimation]()

这段代码声明了一些时间曲线,将在下面的代码中用到:


if shouldEnableRipple {

// Transform.scale

let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")

scaleAnimation.values = [1, 1, 1.05, 1, 1]

scaleAnimation.keyTimes = TileView.rippleAnimationKeyTimes

scaleAnimation.timingFunctions = [linearFunction, timingFunction, timingFunction, linearFunction]

scaleAnimation.beginTime = 0.0

scaleAnimation.duration = duration

animations.append(scaleAnimation)

// Position

let positionAnimation = CAKeyframeAnimation(keyPath: "position")

positionAnimation.duration = duration

positionAnimation.timingFunctions = [linearFunction, timingFunction, timingFunction, linearFunction]

positionAnimation.keyTimes = TileView.rippleAnimationKeyTimes

positionAnimation.values = [zeroPointValue, zeroPointValue, NSValue(CGPoint:rippleOffset), zeroPointValue, zeroPointValue]

positionAnimation.additive = true

animations.append(positionAnimation)

}

shouldEnableRipple是一个布尔值,用来控制是否需要添加形状与位置的变化到动画里面。在之前提到的renderTileViews()方法里面,这个值已经被设置为了true

透明度的动画:


// Opacity

let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity")

opacityAnimation.duration = duration

opacityAnimation.timingFunctions = [easeInOutTimingFunction, timingFunction, timingFunction, easeOutFunction, linearFunction]

opacityAnimation.keyTimes = [0.0, 0.61, 0.7, 0.767, 0.95, 1.0]

opacityAnimation.values = [0.0, 1.0, 0.45, 0.6, 0.0, 0.0]

animations.append(opacityAnimation)

这段代码非常直接了当的设置了不同的透明度在不同的关键帧上keyTimes

把这些动画组合起来赋值给TileView


// Group

let groupAnimation = CAAnimationGroup()

groupAnimation.repeatCount = Float.infinity

groupAnimation.fillMode = kCAFillModeBackwards

groupAnimation.duration = duration

groupAnimation.beginTime = beginTime + rippleDelay

groupAnimation.removedOnCompletion = false

groupAnimation.animations = animations

groupAnimation.timeOffset = kAnimationTimeOffset

layer.addAnimation(groupAnimation, forKey: "ripple")

根据shouldEnableRipple的值,这个动画组合可能有一个或者三个动画。

TileGridView中调用每个子视图TileView来开启这段动画。找到TileGridView.swift的`startAnimatingWithBeginTime(_:):

`方法写入:


private func startAnimatingWithBeginTime(beginTime: NSTimeInterval) {

for tileRows in tileViewRows {

for view in tileRows {

view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: 0, rippleOffset: CGPointZero)

}

}

}

然后运行:

但是这个时候AnimatedULogoView还需要一个波浪式的外推的效果。我们需要给每一个TileView设置一个延迟值。这个值将会根据他们距离中心图层的位置来设定,最后再乘一个常量:

在靠近startAnimatingWithBeginTime(_:)的地方,添加一个段新函数:


private func distanceFromCenterViewWithView(view: UIView)->CGFloat {

guard let centerTileView = centerTileView else { return 0.0 }

let normalizedX = (view.center.x - centerTileView.center.x)

let normalizedY = (view.center.y - centerTileView.center.y)

return sqrt(normalizedX * normalizedX + normalizedY * normalizedY)

}

一个简单计算距离的方法

回到startAnimatingWithBeginTime(_:)写入下面的代码:


for tileRows in tileViewRows {

for view in tileRows {

let distance = self.distanceFromCenterViewWithView(view)

view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: CGPointZero)

}

}

这段代码调用distanceFromCenterViewWithView(_:)后再计算相应的每个视图的延迟时间

运行:

越来接近我们想要的效果了。但还是缺失了点什么。那就是矢量性质的移动。每一个TileView需要根据波浪的向外推动原理,设置一个矢量性的移动(大小与方向)。

回想起我们的高中数学(希望ta没有挂的很早😂), 我们可以根据每个一个TileView距离中心图层的位置来获取一个标准化的矢量。

distanceFromCenterViewWithView(_:):附近,添加一个新的函数:


private func normalizedVectorFromCenterViewToView(view: UIView)->CGPoint {

let length = self.distanceFromCenterViewWithView(view)

guard let centerTileView = centerTileView where length != 0 else { return CGPointZero }

let deltaX = view.center.x - centerTileView.center.x

let deltaY = view.center.y - centerTileView.center.y

return CGPoint(x: deltaX / length, y: deltaY / length)

}

然后回到startAnimatingWithBeginTime(_:),修改之前的代码为:


private func startAnimatingWithBeginTime(beginTime: NSTimeInterval) {

for tileRows in tileViewRows {

for view in tileRows {

let distance = self.distanceFromCenterViewWithView(view)

var vector = self.normalizedVectorFromCenterViewToView(view)

vector = CGPoint(x: vector.x * kRippleMagnitudeMultiplier * distance, y: vector.y * kRippleMagnitudeMultiplier * distance)

view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: vector)

}

}

}

这段代码计算每个子图层所需要的偏差矢量,然后带入到动画调用中。

运行查看一下:

酷炫!?最后一步,是让用户有一种代入感!我们可以在动画开始之前做一个拉伸的效果!

找到startAnimatingWithBeginTime(_:)方法, 添加下面的代码:


let linearTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

let keyframe = CAKeyframeAnimation(keyPath: "transform.scale")

keyframe.timingFunctions = [linearTimingFunction, CAMediaTimingFunction(controlPoints: 0.6, 0.0, 0.15, 1.0), linearTimingFunction]

keyframe.repeatCount = Float.infinity;

keyframe.duration = kAnimationDuration

keyframe.removedOnCompletion = false

keyframe.keyTimes = [0.0, 0.45, 0.887, 1.0]

keyframe.values = [0.75, 0.75, 1.0, 1.0]

keyframe.beginTime = beginTime

keyframe.timeOffset = kAnimationTimeOffset

containerView.layer.addAnimation(keyframe, forKey: "scale")

运行:

有没有很有感觉!你已经实现了一个激动人心的,专业级别的动画。

你可以去尝试改变kRippleMagnitudeMultiplierkRippleDelayMultiplier的值,看看会带来什么样的奇妙化学反应。

最后,回到RootContainerViewController.swift.viewDidLoad()方法。把我们之前的showSplashViewControllerNoPing()调用改为showSplashViewController()

再次运行:

一个酷炫的开屏动画就这样被创建了出来。有没有感到热血沸腾呢!

其他

你可以在这里下载完整项目

如果你想学到更多动画的姿势!请戳这里

Have Fun!

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

推荐阅读更多精彩内容