Getting started with UIKit Dynamics in Swift


As you may already know, Swift, the new programming language from Apple, has been out for quite a while and almost every iOS developer is today having the itch to make the most out of it.

The first thing you’ll notice in the language is its similarity to most of the dot-notation based languages out there, like JavaScript, ActionScript, Java and so on. This is a massive advantage for people who didn’t have the guts to approach iOS development because of the complexity of Objective-C. Now everyone with a bit of programming experience is ready.

I won’t get into details about the language itself. If you’re new to the language I suggest to read the Swift Cheat Sheet, or check out Design+Code by Meng To, which I strongly recommend, especially for beginners. If you want to dig deeper, you can download the official E-Book from Apple, free of charge.

In this post I’m going to talk about UIKit Dynamics, a framework used to create complex 2D animations. While we could use the Core Animation framework to perform basic transitions, the new Dynamics framework includes real-world physics behaviors out of the box, like gravity, collision, density and so on. The UIKit Dynamics framework is also very easy to implement, which means you don’t have to be a particle physicist to get started.

So let’s start with a simple idea looking at the real world around us. Imagine you’re standing up holding a box with both your hands perpendicularly to your body. The box has a specific weight and it is made of a specific material. Also there is gravity that is trying to pull it down on the floor, so you’re applying some kind of resistance or force to hold it. If you let the box go, it falls with a certain speed until it hits the floor which is also preventing it to keep falling forever. When the box hits the ground, chances are it won’t just hit and stop. It might bounce around for a while based on it’s velocity, density or bounciness, or just crash into a million pieces.

This makes sense, but how do we simulate this from within an application? The image below shows the final demo.

Let’s assume you already have Xcode 6 Beta so you can actually write Swift Apps and you already have a UIViewController ready to start with. Now let’s create a box:

import UIKit

class Main: UIViewController {
    
    // Declare the box View
    var greenBox: UIView?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Instantiate the box View
        self.greenBox = UIView();
        
        // Make it green
        self.greenBox!.backgroundColor = UIColor.greenColor();
        
        // Place it in the center of our screen
        self.greenBox!.frame = CGRectMake(CGRectGetMidX(self.view.frame) - 50, CGRectGetMidY(self.view.frame) - 50, 100, 100)
        
        self.view.addSubview(self.greenBox);
    }
}

Go ahead and run this code. You should have a green box sitting in the middle of your iOS simulator. Now let’s think. This box shouldn’t just float in the air like a ghost, in the real world objects tend to fall (and there are no ghosts). So let’s add some gravity behavior to our box.

import UIKit

class Main: UIViewController {
    
    // Declare the box View
    var greenBox: UIView?
    
    var animator: UIDynamicAnimator?
    var gravity: UIGravityBehavior?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Instantiate the box View
        self.greenBox = UIView();
        
        // Make it green
        self.greenBox!.backgroundColor = UIColor.greenColor();
        
        // Place it in the center of our screen
        self.greenBox!.frame = CGRectMake(CGRectGetMidX(self.view.frame) - 50, CGRectGetMidY(self.view.frame) - 50, 100, 100)
        
        self.view.addSubview(self.greenBox);
        
        // Instantiates the animator
        self.animator = UIDynamicAnimator(referenceView: self.view);
        
        // Instantiates the Gravity Behavior and assigns the box to it
        self.gravity = UIGravityBehavior(items: [self.greenBox!]);
    }
}

As you see at line 8, we are declaring a UIDynamicAnimator which is the backbone of our Dynamic engine. It essentially takes care of each and every behavior we add to our UIViews. At line 9 then, we are adding a UIGravityBehavor to assign to the box later at line 29. Of course we also instantiated the animator at line 26. However, if you run the app, the box won’t fall. This because we didn’t add the gravity behavior to our animator yet.

Understanding Dynamic Behaviors

There is a very important concept we need to understand before going ahead. You might think that the gravity behavior for instance, should be added to a parent container, and any view inside of that container should undergo the gravity force and thus start falling. That’s not correct.

Each Dynamic behavior is assigned to a UIView instead. So in our case we are adding it to our greenBox instance, which will be affected by gravity. But we are not done yet. In the above code we just instantiated the animator and the gravity behavior. Now we need to pass the gravity behavior to the animator, which will animate the view accordingly:

import UIKit

class Main: UIViewController {
    
    // Declare the box View
    var greenBox: UIView?
    
    var animator: UIDynamicAnimator?
    var gravity: UIGravityBehavior?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Instantiate the box View
        self.greenBox = UIView();
        
        // Make it green
        self.greenBox!.backgroundColor = UIColor.greenColor();
        
        // Place it in the center of our screen
        self.greenBox!.frame = CGRectMake(CGRectGetMidX(self.view.frame) - 50, CGRectGetMidY(self.view.frame) - 50, 100, 100)
        
        self.view.addSubview(self.greenBox);
        
        // Instantiates the animator
        self.animator = UIDynamicAnimator(referenceView: self.view);
        
        // Instantiates the Gravity Behavior and assigns the box to it
        self.gravity = UIGravityBehavior(items: [self.greenBox!]);
        
        // There we go!
        self.animator!.addBehavior(self.gravity)
    }
}

Check line 32 from the above snippet, run the app and try to catch the box if you can. But the box keeps falling past the screen boundaries, and that’s because there is nothing to contain it. Our floor is not there.

Adding a collision behavior

With that said, let’s add a Collision Behavior to our box and make the boundaries of the screen be the boundaries.

Again, in the same way we added the Gravity behavior, but without adding another animator instance:

import UIKit

class Main: UIViewController {
    
    // Declare the box View
    var greenBox: UIView?
    
    var animator: UIDynamicAnimator?
    var gravity: UIGravityBehavior?
    var collision: UICollisionBehavior?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Instantiate the box View
        self.greenBox = UIView();
        
        // Make it green
        self.greenBox!.backgroundColor = UIColor.greenColor();
        
        // Place it in the center of our screen
        self.greenBox!.frame = CGRectMake(CGRectGetMidX(self.view.frame) - 50, CGRectGetMidY(self.view.frame) - 50, 100, 100)
        
        self.view.addSubview(self.greenBox);
        
        // Instantiates the animator
        self.animator = UIDynamicAnimator(referenceView: self.view);
        
        // Instantiates the Gravity Behavior and assigns the box to it
        self.gravity = UIGravityBehavior(items: [self.greenBox!]);
        
        // Instantiates the Collision Behavior and assigns the box to it
        self.collision = UICollisionBehavior(items: [self.greenBox!]);
        self.collision!.translatesReferenceBoundsIntoBoundary = true;
        
        // There we go!
        self.animator!.addBehavior(self.gravity)
        self.animator!.addBehavior(self.collision)
    }
}

We declared our collision Behavior at line 10 and instantiated it at line 33. We also set self.collision!.translatesReferenceBoundsIntoBoundary to true, which will set a collision boundary according to the bounds of the dynamic animator’s coordinate system (in our case the boundaries of self.view, see line 27).

Now the green box should fall and hit the bottom of the screen, bounce a little bit and stop there. But that’s boring isn’t it? Let’s add some interactivity to the box. We want to be able to grab the box and let it go from different places in our screen, so let’s write that.

Adding a pan gesture to the box

Let’s declare a UIPanGestureRecognizer and instantiate it. Then we will add it to our box:

import UIKit

class Main: UIViewController {
    
    // Declare the box View
    var greenBox: UIView?
    
    var animator: UIDynamicAnimator?
    var gravity: UIGravityBehavior?
    var collision: UICollisionBehavior?
    
    var panGesture: UIPanGestureRecognizer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Instantiate the box View
        self.greenBox = UIView();
        
        // Make it green
        self.greenBox!.backgroundColor = UIColor.greenColor();
        
        // Place it in the center of our screen
        self.greenBox!.frame = CGRectMake(CGRectGetMidX(self.view.frame) - 50, CGRectGetMidY(self.view.frame) - 50, 100, 100)
        
        self.view.addSubview(self.greenBox);
        
        // Instantiates the animator
        self.animator = UIDynamicAnimator(referenceView: self.view);
        
        // Instantiates the Gravity Behavior and assigns the box to it
        self.gravity = UIGravityBehavior(items: [self.greenBox!]);
        
        // Instantiates the Collision Behavior and assigns the box to it
        self.collision = UICollisionBehavior(items: [self.greenBox!]);
        self.collision!.translatesReferenceBoundsIntoBoundary = true;
        
        // There we go!
        self.animator!.addBehavior(self.gravity)
        self.animator!.addBehavior(self.collision)
        
        // Instantiates the Pan Gesture Recognizers and adds it to the greenBox instance
        self.panGesture = UIPanGestureRecognizer(target: self, action: "panning:");
        self.greenBox!.addGestureRecognizer(panGesture);
    }
}

We instantiated the pan gesture object at line 43 and added it to the greenBox at line 44. We also set a target function that will handle the pan gesture. Let’s add this function below the viewDidLoad() function as follows:

// viewDidLoadCodeHere

func panning(pan: UIPanGestureRecognizer) {
    println("Our box is panning...");
    var location = pan.locationInView(self.view);
    var touchLocation = pan.locationInView(self.greenBox);
}

If you’re not familiar with gesture recognizers, we are now handling any pan gesture that might apply to the greenBox view. Every time you try to drag the box with your finger (or the mouse if you use a simulator), the panning() function is called, tracing out a message in the console. We are also creating two variables, location and touchLocation. The first one stores the location of the dragged point relative to the main view (self.view), while the other stores the location of the dragged point relative to the box itself. We will need the latter in a while. For now let’s handle our pan gesture properly.

Inside the panning() function, we will add three different conditions, each for all three three states of a standard panning gesture.

The Began State:

This state is set by the Pan Gesture Recognizer at the beginning of the dragging operation. When this state is the current state of the pan gesture, some setup code will be performed. Let’s check line number 8:

// viewDidLoadCodeHere

func panning(pan: UIPanGestureRecognizer) {
    println("Our box is panning...");
    var location = pan.locationInView(self.view);
    var touchLocation = pan.locationInView(self.greenBox);
    
    if pan.state == UIGestureRecognizerState.Began {
        // Do some initial setup here
        // Will set the box's center to the location value stored above
        self.greenBox!.center = location;
    }
}

In this case, when the state of the pan gesture is equal to UIGestureRecognizerState.Began we can match the center of the box to the touch point. The box will be dragged just for a couple of pixels then it will stop. That’s because the state of the pan gesture will soon change to UIGestureRecognizerState.Changed. So let’s handle that.

The Changed State:
The Changed state is set by the pan gesture recognizer when the dragging is taking place and thus, the location value is changing continuously. We want to set the box location according to the touch location here as well:

// viewDidLoadCodeHere

func panning(pan: UIPanGestureRecognizer) {
    println("Our box is panning...");
    var location = pan.locationInView(self.view);
    var touchLocation = pan.locationInView(self.greenBox);
    
    if pan.state == UIGestureRecognizerState.Began {
        // Do some initial setup here
        // Will set the box's center to the location value stored above
        self.greenBox!.center = location;
        
    }else if pan.state == UIGestureRecognizerState.Changed {
        self.greenBox!.center = location;
        
    }
}

This is pretty self explanatory. If you run the app, you should be able to drag the box normally by now. When the state of the pan gesture is set to UIGestureRecognizerState.Changed, the location of the box is updated repeatedly resulting in a smooth drag. You may have noticed that if you let the box go, it won’t fall. That’s because the gravity behavior has to be reset in the Ended state.

The Ended State:

When we release our finger off the box, the state of the pan gesture is set to UIGestureRecognizerState.Ended. We will need to add the gravity behavior to the animator so that the box will start falling.

// viewDidLoadCodeHere

func panning(pan: UIPanGestureRecognizer) {
    println("Our box is panning...");
    var location = pan.locationInView(self.view);
    var touchLocation = pan.locationInView(self.greenBox);
    
    if pan.state == UIGestureRecognizerState.Began {
        // Do some initial setup here
        
        //Removes all the behaviors attached to the animators for now
        self.animator!.removeAllBehaviors()
        
        // Will set the box's center to the location value stored above
        self.greenBox!.center = location;
        
    }else if pan.state == UIGestureRecognizerState.Changed {
        self.greenBox!.center = location;
    }else if pan.state == UIGestureRecognizerState.Ended {
        // Handles what should happen when the box is released...
        self.animator!.addBehavior(self.gravity)
        self.animator!.addBehavior(self.collision)
    }
}

In the code above we also removed all the behaviors attached to the animator (line 12) because of course, we don’t want the gravity to pull the box from our fingers when we drag it. In this case we will also have to add the collision behavior again at line 22. Of course we can just remove the gravity behavior alone using self.animator!.removeBehavior(self.gravity) but for the purpose of this tutorial, we will leave it just as is. If you test the app now, the box should fall once released, colliding to the screen boundaries.

Now let’s make it more interesting. We have added two behaviors to our box. Gravity and collision. Let’s add a UIAttachmentBehavior to it, so that the dragging becomes more realistic. It will look just like we are pulling around the box instead of just dragging it. However, if you look closely to the box when you first drag it, you will notice that it always moves so that the touch point is always on its center. That’s because we are setting its center as the location of the touch point. To fix that we will need to compensate the touch location by adding an offset, and that is when the touchLocation variable comes in handy. We’ll get to that in a minute.

The Attachment Behavior

We will declare the new behavior and implement it in our panning() function:

// viewDidLoadCodeHere

var attach: UIAttachmentBehavior?

func panning(pan: UIPanGestureRecognizer) {
    println("Our box is panning...");
    var location = pan.locationInView(self.view);
    var touchLocation = pan.locationInView(self.greenBox);
    
    if pan.state == UIGestureRecognizerState.Began {
        self.animator!.removeAllBehaviors()
        
        var offset = UIOffsetMake(touchLocation.x - CGRectGetMidX(self.greenBox!.bounds), touchLocation.y - CGRectGetMidY(self.greenBox!.bounds))
        self.attach = UIAttachmentBehavior(item: self.greenBox, offsetFromCenter: offset, attachedToAnchor: location)
        self.animator!.addBehavior(self.attach);
        
    }else if pan.state == UIGestureRecognizerState.Changed {
        self.attach!.anchorPoint = location;
        
    }else if pan.state == UIGestureRecognizerState.Ended {
        self.animator!.removeBehavior(self.attach)
        
        self.animator!.addBehavior(self.gravity)
        self.animator!.addBehavior(self.collision)
    }
}

What’s going on in the above snippet is pretty straightforward. I’ve refactored the code a little but inside of the state conditions. at line 11 we first we remove all the behaviors from the animator. It’s important that we remove the behavior inside the began state and not above it.

At line 13 we set an offset variable which holds the offset to calculate when the box is touched. With this the touch point won’t snap to the center of the box but will occur where expected.

At line 14, we instantiate the attachment behavior. The arguments passed are the box, the offset used and the location of the touch point.

At line 15, we finally add the attachment behavior to the animator. At line 18 we set the anchor point of the attach behavior to the location value of the pan gesture. This way, the anchor point will result exactly at the touch point location inside the green box. Of course we need this to be whenever the pan gesture state is changing, so it updates repeatedly.

At line 21, when the state of the pan gesture ends, we need to remove the attachment behavior, so that the box doesn’t remain “attached” somewhere. This way the gravity and collision behaviors will take place.

Now run the app and drag the box around the screen. See how it rotates around the anchor point as we explained above.

How about adding the ability to “throw” the box around instead? By now, whenever you let it go, it just falls down. All we need to do is to add a UIDynamicItemBehavior. This behavior is an auxiliary behavior that can be added to UIViews, whenever we want to add extra properties like linearVelocity or elasticity, and so on. We will declare this behavior when the pan gesture is ending:

// viewDidLoadCodeHere

var attach: UIAttachmentBehavior?

func panning(pan: UIPanGestureRecognizer) {
    println("Our box is panning...");
    var location = pan.locationInView(self.view);
    var touchLocation = pan.locationInView(self.greenBox);
    
    if pan.state == UIGestureRecognizerState.Began {
        self.animator!.removeAllBehaviors()
        
        var offset = UIOffsetMake(touchLocation.x - CGRectGetMidX(self.greenBox!.bounds), touchLocation.y - CGRectGetMidY(self.greenBox!.bounds))
        self.attach = UIAttachmentBehavior(item: self.greenBox, offsetFromCenter: offset, attachedToAnchor: location)
        self.animator!.addBehavior(self.attach);
        
    }else if pan.state == UIGestureRecognizerState.Changed {
        self.attach!.anchorPoint = location;
        
    }else if pan.state == UIGestureRecognizerState.Ended {
        self.animator!.removeBehavior(self.attach)
        
        var itemBehavior = UIDynamicItemBehavior(items: [self.greenBox!]);
        itemBehavior.addLinearVelocity(pan.velocityInView(self.view), forItem: self.greenBox);
        itemBehavior.angularResistance = 0;
        itemBehavior.elasticity = 0.8;
        self.animator!.addBehavior(itemBehavior);
        
        self.animator!.addBehavior(self.gravity)
        self.animator!.addBehavior(self.collision)
    }
}

In the snippet above we are instantiating a UIDynamicItemBehavior at line 23.

At line 24 we are adding a linear velocity to the box so that whenever we release our finger, the box follows its original direction. We have to pass to addLinearVelocity the velocity value taken from the pan gesture and the box itself. The linear velocity will decrease over time because of the gravity behavior

At line 25, we are adding an angular resistance of 0, which means no angular resistance at all. The higher the value is, the more the box will refrain from rotating. Feel free to play around with this value.

At line 26, we are giving the box some elasticity value. The value expected should be a float number from 0.0 to 1.0. 1.0 means the box will be super bouncy. 0.0, means it will bounce very little.

At line 27, we finally add the item behavior to the animator as usual, along with the other ones.

To recap, the final code should look as follows:

import UIKit

class Main: UIViewController {
    
    
    var greenBox: UIView?
    
    var animator: UIDynamicAnimator?
    var attach: UIAttachmentBehavior?
    var gravity: UIGravityBehavior?
    var collision: UICollisionBehavior?
    
    var panGesture: UIPanGestureRecognizer?
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.greenBox = UIView();
        self.greenBox!.backgroundColor = UIColor.greenColor();
        self.greenBox!.frame = CGRectMake(CGRectGetMidX(self.view.frame) - 50, CGRectGetMidY(self.view.frame) - 50, 100, 100)
        self.view.addSubview(self.greenBox);
        
        self.animator = UIDynamicAnimator(referenceView: self.view);
        
        self.gravity = UIGravityBehavior(items: [self.greenBox!]);
        
        self.collision = UICollisionBehavior(items: [self.greenBox!]);
        self.collision!.translatesReferenceBoundsIntoBoundary = true;
        
        self.panGesture = UIPanGestureRecognizer(target: self, action: "panning:");
        self.greenBox!.addGestureRecognizer(panGesture);
        
    }
    
    func panning(pan: UIPanGestureRecognizer) {
        var location = pan.locationInView(self.view)
        var touchLocation = pan.locationInView(self.greenBox);
        
        if pan.state == UIGestureRecognizerState.Began {
            self.animator!.removeAllBehaviors()
            
            var offset = UIOffsetMake(touchLocation.x - CGRectGetMidX(self.greenBox!.bounds), touchLocation.y - CGRectGetMidY(self.greenBox!.bounds))
            self.attach = UIAttachmentBehavior(item: self.greenBox, offsetFromCenter: offset, attachedToAnchor: location)
            self.animator!.addBehavior(self.attach);
            
        }else if pan.state == UIGestureRecognizerState.Changed {
            self.attach!.anchorPoint = location;
            
        }else if pan.state == UIGestureRecognizerState.Ended {
            self.animator!.removeBehavior(self.attach)
            
            var itemBehavior = UIDynamicItemBehavior(items: [self.greenBox!]);
            itemBehavior.addLinearVelocity(pan.velocityInView(self.view), forItem: self.greenBox);
            itemBehavior.angularResistance = 0;
            itemBehavior.elasticity = 0.8;
            self.animator!.addBehavior(itemBehavior);
            
            self.animator!.addBehavior(self.gravity)
            self.animator!.addBehavior(self.collision)
        }
    }
}

Conclusion

As you see, we could achieve pretty good results with a few lines of code. The UIKit Dynamics framework, however, shouldn’t be used for game development but only to add fancy animations to UIViews in regular iOS applications. If you’re more into game development you should use SpriteKit, which performs better in rendering a large amount views using OpenGL. Both frameworks share the same Physics engine though.

You can download the Xcode project from here, which includes a bonus view that collides with the green box. In the next post I will cover the last two behaviors, UIPushBehavior and UISnapBehavior and of course, don’t hesitate to contact me if you have questions.

From:

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

推荐阅读更多精彩内容