貝塞爾曲線在iOS端的繪圖實踐

1 前言

在這個大數據的時代,很多信息只有通過圖形才能更好的展示給用戶。例如:房屋的歷史價格、基金股票的歷史增長、數據佔比分析圖等。如何做圖形?需要用到什麼知識?本文將從 建模、顯示 兩方面來展開介紹。

2 建模

建模是一切圖形的基礎,其他內容的前提,要用代碼展示一個圖形,首先要有它的幾何模型表達。目前在客戶端二維圖形建模上,Bézier curve(貝塞爾曲線)可以稱爲 經典 和 主流 並重的數學曲線。

對於貝塞爾曲線來說,最重要的是 起始點、終止點(也稱錨點)、控制點。控制點決定了一條路徑的彎曲軌跡,根據控制點的個數,貝塞爾曲線被分爲:一階貝塞爾曲線(0個控制點)、二階貝塞爾曲線(1個控制點)、三階貝塞爾曲線(2個控制點)、N階貝塞爾曲線(n - 1個控制點)。

2.1 貝塞爾曲線原理

以二階貝塞爾曲線爲例 解釋說明:

起始點:P0 ; 控制點:P1 ; 終止點:P2

  1. 連接P0P1線 和 P1P2線。
  2. 在P0P1線上找到點A,在P1P2線上找到點B,使得 P0A/AP1 = P1B/BP2
  3. 連接AB,在AB上找到點X,X點滿足:AX/XB = P0A/AP1 = P1B/BP2
  4. 找出所有滿足公式:AX/XB = P0A/AP1 = P1B/BP2 的X點。(從P0 到 P2的紅色曲線點爲所有X點的連線)這條由所有X點組成的連線 即爲 貝塞爾曲線。

二階貝塞爾曲線 起始點:P0 ; 控制點:P1 ; 終止點:P2

三階貝塞爾曲線 起始點:P0 ; 控制點:P1、P2; 終止點:P3

四階貝塞爾曲線 起始點:P0 ; 控制點:P1、P2、P3 ; 終止點:P4

2.2 UIBezierPath類

系統給我們提供了一個叫做UIBezierPath類,用它可以畫簡單的圓形,橢圓,矩形,圓角矩形,也可以通過添加點去生成任意的圖形,還可以簡單的創建一條二階貝塞爾曲線和三階貝塞爾曲線。我們來了解一下它的常用方法:

2.2.1 初始化方法

// 創建UIBezierPath對象
+ (instancetype)bezierPath;
// 創建在rect內的矩形
+ (instancetype)bezierPathWithRect:(CGRect)rect;
// 設定特定的角爲圓角的矩形,corners:指定的角爲圓角,其他角不變,cornerRadii:圓角的大小
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
// 創建圓弧
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// 通過已有路徑創建路徑
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
// 創建三次貝塞爾曲線 endPoint:終點 controlPoint1:控制點1 controlPoint2:控制點2
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
- // 創建二次貝塞爾曲線 endPoint:終點 controlPoint:控制點
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;

2.2.2 使用方法

// 移動到某一點
- (void)moveToPoint:(CGPoint)point;
// 繪製一條線
- (void)addLineToPoint:(CGPoint)point;
// 閉合路徑,即在終點和起點連一根線
- (void)closePath;
// 清空路徑
- (void)removeAllPoints;
// 填充
- (void)fill;
// 描邊,路徑創建需要描邊才能顯示出來
- (void)stroke;

2.2.3 常用屬性

// 將UIBezierPath類轉換成CGPath,類似於UIColor的CGColor
@property(nonatomic) CGPathRef CGPath;
// path線的寬度
@property(nonatomic) CGFloat lineWidth;
// path端點樣式
@property(nonatomic) CGLineCap lineCapStyle;
// 拐角樣式
@property(nonatomic) CGLineJoin lineJoinStyle;

2.2.4 舉個栗子🌰

先看效果👇:

代碼如下:

- (void)drawRect:(CGRect)rect {

    [[UIColor redColor] set];

    // 右邊第一個圖
    UIBezierPath* maskPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(50, 50, 100, 100) byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(30, 30)];
    maskPath.lineWidth     = 20.f;
    maskPath.lineJoinStyle = kCGLineJoinBevel;
    [maskPath stroke];
    // 中間第二個圖
    UIBezierPath* maskFillPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(200, 50, 100, 100) byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(30, 30)];
    maskFillPath.lineWidth     = 20.f;
    maskFillPath.lineJoinStyle = kCGLineJoinBevel;
    [maskFillPath fill];
    [maskFillPath stroke];
    // 右邊第三個圖
    UIBezierPath *maskLinePath = [UIBezierPath bezierPath];
    maskLinePath.lineWidth     = 20.f;
    maskLinePath.lineCapStyle  = kCGLineCapRound;
    [maskLinePath moveToPoint:CGPointMake(250.0, 50)];
    [maskLinePath addLineToPoint:CGPointMake(300.0, 100.0)];
    [maskLinePath stroke];
}

上圖中:

1)圖一和圖二 唯一的不同是[maskFillPath fill]方法,fill方法要在封閉的曲線調用。

2)圖一和圖二 爲設定特定的角爲圓角的矩形,corners爲UIRectCornerTopLeft左上角,cornerRadii圓角大小爲30,綠色的箭頭 表示的設定的這個角。

corners 爲下面五種類型
typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft     = 1 << 0,    // 左上角
    UIRectCornerTopRight    = 1 << 1,    // 右上角
    UIRectCornerBottomLeft  = 1 << 2,    // 左下角
    UIRectCornerBottomRight = 1 << 3,    // 右下角
    UIRectCornerAllCorners  = ~0UL        // 全部
};

3)圖一和圖二 黃色的箭頭 設置的屬性 拐角樣式:lineJoinStyle kCGLineJoinBevel(缺角)

lineJoinStyle 爲下面三種類型
typedef CF_ENUM(int32_t, CGLineJoin) {
    kCGLineJoinMiter,    // 尖角
    kCGLineJoinRound,    // 圓角
    kCGLineJoinBevel    // 缺角
};

4)圖三 白色的箭頭 設置的屬性 path端點樣式:lineCapStyle kCGLineCapRound(圓形端點)

lineCapStyle 爲下面三種類型
typedef CF_ENUM(int32_t, CGLineCap) {
    kCGLineCapButt,        // 無端點
    kCGLineCapRound,    // 圓形端點
    kCGLineCapSquare    // 方形端點
};

有興趣的 可以試試別的方法屬性~

2.3 波浪曲線實現

如何實現 N階 波浪式曲線?如何找到 N-1 個對應的控制點?

有兩個方法,下圖爲同數據,方案一 和 方案二 分別所得曲線圖。

方案一 爲左邊(三階貝塞爾)圖 其中 第二條的紅點 爲數據的位置

方案二 爲右邊(CatmullRom)圖 其中 第二條的紅點 爲數據的位置

方案一:根據 創建三次貝塞爾曲線 方法 實現波浪曲線

控制點的選取方案不唯一,以下爲我選擇控制點的方案:

控制點P1:CGPointMake((PrePonit.x+NowPoint.x)/2, PrePonit.y)

控制點P2:CGPointMake((PrePonit.x+NowPoint.x)/2, NowPoint.y)

可以根據前一個點PrePonit 和 現在的點NowPoint 進行計算 控制點。

主要代碼如下:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:[self pointAtIndex:0]];
NSInteger count = self.points.count;
CGPoint PrePonit;

for (NSInteger i = 0; i < count; i++) {

    CGPoint NowPoint = [self pointAtIndex:i];
    if(i == 0) {
        PrePonit = NowPoint;
    } else {
          // 利用三次曲線 形成波浪曲線
        [path addCurveToPoint:point controlPoint1:CGPointMake((PrePonit.x+NowPoint.x)/2, PrePonit.y) controlPoint2:CGPointMake((PrePonit.x+NowPoint.x)/2, NowPoint.y)];
        PrePonit = NowPoint;
    }
}
return path;

方案二:使用 CatmullRom 插值樣條。(有興趣的可以百度一下~這裏只簡單介紹)

要點分析:

1)給定一組控制點而得到一條曲線,曲線經過給定所有數據點。

2)Catmull-Rom 公式:P(t) = 0.5 * (2*p1 + (p2 - p0) *t + (2*p0 - 5*p1 + 4*p2 - p3)* t * t + (3*p1 - p0 - 3*p2 + p3) * t * t * t);

注:t爲分割的最小粒尺寸,根據 P0 P1 P2 P3 順序的四個點 求得 P1P2 曲線公式。

主要代碼如下:

void getPointsFromBezier(void *info, const CGPathElement *element) {
    NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info;
    CGPathElementType type = element->type;
    CGPoint *points = element->points;

    if (type != kCGPathElementCloseSubpath) {
        [bezierPoints addObject:VALUE(0)];
        if ((type != kCGPathElementAddLineToPoint) &&
            (type != kCGPathElementMoveToPoint))
            [bezierPoints addObject:VALUE(1)];
    }
    if (type == kCGPathElementAddCurveToPoint)
        [bezierPoints addObject:VALUE(2)];
}

NSArray *pointsFromBezierPath(UIBezierPath *bpath) {
    NSMutableArray *points = [NSMutableArray array];
    // 獲取貝塞爾曲線上所有的點
    CGPathApply(bpath.CGPath, (__bridge void *)points, getPointsFromBezier);
    return points;
}

- (UIBezierPath*)smoothedPathWithGranularity:(NSInteger)granularity path:(UIBezierPath *)path {
    NSMutableArray *points = [pointsFromBezierPath(path) mutableCopy];

    if (points.count < 4) return [path copy];
    [points insertObject:[points objectAtIndex:0] atIndex:0];
    [points addObject:[points lastObject]];

    UIBezierPath *smoothedPath = [path copy];
    [smoothedPath removeAllPoints];
    [smoothedPath moveToPoint:POINT(0)];

    for (NSUInteger index = 1; index < points.count - 2; index++) {
        CGPoint p0 = POINT(index - 1);
        CGPoint p1 = POINT(index);
        CGPoint p2 = POINT(index + 1);
        CGPoint p3 = POINT(index + 2);

        for (int i = 1; i < granularity; i++) {
              // granularity 這裏按照 20 粒度劃分的
            float t = (float) i * (1.0f / (float) granularity);
            float tt = t * t;
            float ttt = tt * t;

            CGPoint pi;
            // 根據 CatmullRom 公式 根據 P0 P1 P2 P3 獲取點的座標
            pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt);
            pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt);
            if (pi.x <= self.width) {
                [smoothedPath addLineToPoint:pi];
            }
        }

        if (p2.x <= self.width) {
            [smoothedPath addLineToPoint:p2];
        }
    }

    return smoothedPath;
}

對比總結:

方案一 控制點的選取比較難,曲線的彎度 也取決於控制點,操作簡單,易理解。

方案二 曲線更順滑,但實現更復雜,不易理解。

這裏推薦兩個好用的網站:

a 這個網站提供了豐富的曲線類型可供選擇,非常直觀。

http://easings.net

b 這個網站提供了可視化的修改兩個控制點,來生成一條三階貝塞爾曲線,並切右邊還可以看到這條曲線產生的動畫會做怎樣的速度改變。

http://www.roblaplaca.com/examples/bezierBuilder/#

3 顯示

當layer與貝塞爾曲線相遇,會發生什麼樣的神奇反應?

3.1 CALayer

蘋果官網註釋:“An object that manages image-based content and allows you to perform animations on that content.” 管理基於圖像的內容並允許您對該內容執行動畫的對象。

CALayer 主要就兩方面作用:

1) 管理展示內容

2)內容可執行動畫

CALayer 自身有很多情況下自帶隱式動畫,但是UIView的根Layer是沒有隱式動畫的。

3.1.1 常用屬性

// 圖層大小 支持隱式動畫
@property CGRect bounds;
// 圖層位置 支持隱式動畫
@property CGPoint position;
// 在z軸上的位置 支持隱式動畫
@property CGFloat zPosition;
// 沿z軸位置的錨點 支持隱式動畫
@property CGFloat anchorPointZ;
// 錨點 默認在layer的中心點 取值範圍(0~1) 支持隱式動畫
@property CGPoint anchorPoint;
// 圖層變換 支持隱式動畫
@property CATransform3D transform;
// 圖層大小和位置 不支持隱式動畫
@property CGRect frame;
// 是否隱藏 支持隱式動畫
@property(getter=isHidden) BOOL hidden;
// 圖層背景是否顯示 支持隱式動畫
@property(getter=isDoubleSided) BOOL doubleSided;
// 父圖層 支持隱式動畫
@property(nullable, readonly) CALayer *superlayer;
// 子圖層 支持隱式動畫
@property(nullable, copy) NSArray<__kindof CALayer *> *sublayers;
// 子圖層變換 支持隱式動畫
@property CATransform3D sublayerTransform;
// 圖層蒙版 支持隱式動畫
@property(nullable, strong) __kindof CALayer *mask;
// 子圖層是否裁切超出父圖層的部分,默認爲NO
@property BOOL masksToBounds;
// 圖層顯示內容 設置layer的contents可以爲layer添加顯示內容 支持隱式動畫
@property(nullable, strong) id contents;
// 圖層顯示內容的大小和位置 支持隱式動畫
@property CGRect contentsRect;
// 用於指定層的內容如何在其範圍內定位或縮放
@property(copy) CALayerContentsGravity contentsGravity;
// 是否包含完全不透明內容的布爾值
@property(getter=isOpaque) BOOL opaque;
// 背景色 支持隱式動畫
@property(nullable) CGColorRef backgroundColor;
// 圓角半徑 支持隱式動畫
@property CGFloat cornerRadius;
// 邊框寬度 支持隱式動畫
@property CGFloat borderWidth;
// 邊框顏色 支持隱式動畫
@property(nullable) CGColorRef borderColor;
// 透明度 支持隱式動畫
@property float opacity;
// 陰影顏色 支持隱式動畫
@property(nullable) CGColorRef shadowColor;
// 陰影透明度 默認爲0 需要顯示陰影 必須設置值 支持隱式動畫
@property float shadowOpacity;
// 陰影偏移量 支持隱式動畫
@property CGSize shadowOffset;
// 陰影半徑 支持隱式動畫
@property CGFloat shadowRadius;
// 陰影形狀 支持隱式動畫
@property(nullable) CGPathRef shadowPath;

3.1.2 子類

CALayer的子類有很多,下面說幾個比較常用的。

3.2 CAShapeLayer

蘋果官網註釋:“A layer that draws a cubic Bezier spline in its coordinate space.” 專門用於繪製貝塞爾曲線的layer。

3.2.1 看一下它獨特的屬性:

// path屬性是曲線的路徑,也是它和貝塞爾曲線緊密連接一個入口,決定了圖層上畫的是什麼形狀。
@property(nullable) CGPathRef path;
// 填充顏色
@property(nullable) CGColorRef fillColor;
// 曲線 指定哪塊區域爲內部,內部會被填充顏色
@property(copy) CAShapeLayerFillRule fillRule;
// 線的顏色
@property(nullable) CGColorRef strokeColor;
// strokeStart 和 strokeEnd 兩者的取值都是0~1,決定貝塞爾曲線的劃線百分比
@property CGFloat strokeStart;
@property CGFloat strokeEnd;
// 虛線開始的位置
@property CGFloat lineDashPhase;
// 虛線設置,數組中奇數位實線長度,偶數位帶遍空白長度
@property(nullable, copy) NSArray<NSNumber *> *lineDashPattern;
// 線的寬度
@property CGFloat lineWidth;
// 最大斜接長度 只有lineJoin屬性爲kCALineJoinMiter時miterLimit纔有效
@property CGFloat miterLimit;
// 線端點樣式(樣式與 貝塞爾曲線的CGLineCap 屬性一致)
@property(copy) CAShapeLayerLineCap lineCap;
// 拐角樣式(樣式與 貝塞爾曲線的CGLineJoin 屬性一致)
@property(copy) CAShapeLayerLineJoin lineJoin;

3.2.2 舉個栗子🌰

使用上面的一些屬性,再結合貝塞爾曲線,我們實現瞭如下一些效果:

其中圖五的效果,代碼實現如下:

UIBezierPath *maskPath = [UIBezierPath bezierPath];
for (NSInteger i = 1; i < 9; i++) {
    UIBezierPath *tempPath = [UIBezierPath bezierPathWithRect:CGRectMake(190 - 20 * i, 550 - 10 * i, 40 * i, 20 * i)];
    [maskPath appendPath:tempPath];
}
[maskPath stroke];

// CAShapeLayer
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
maskLayer.lineWidth = 5;
maskLayer.strokeColor = [UIColor purpleColor].CGColor;
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.fillColor = [UIColor cyanColor].CGColor;
maskLayer.strokeStart = 0.2;
maskLayer.strokeEnd = 0.5;
maskLayer.lineDashPattern = @[@(10), @(10), @(30), @(30)];
maskLayer.lineDashPhase = 0;
[self.view.layer addSublayer:maskLayer];

3.3 CAGradientLayer

蘋果官網註釋:“A layer that draws a color gradient over its background color, filling the shape of the layer (including rounded corners)” 專門用於在背景色上繪製顏色漸變的圖層,填充圖層的形狀。

3.3.1 看一下它獨特的屬性:

// colors屬性是CAGradientLayer的特殊屬性,完美實現幾種顏色的過渡。
@property(nullable, copy) NSArray *colors;
// 定義每個梯度停止的位置。取值範圍爲0~1遞增
@property(nullable, copy) NSArray<NSNumber *> *locations;
// 決定了變色範圍的起始點
@property CGPoint startPoint;
// 決定了變色範圍的結束點
@property CGPoint endPoint;
// startPoint 和 endPoint兩者的連線決定變色的趨勢

3.3.2 舉個栗子🌰

使用上面的一些屬性我們實現瞭如下一些效果:

其中圖五的效果,代碼實現如下:

CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(20, 450, 150, 150);
gradientLayer.locations = @[@(0.2), @(0.5), @(0.6), @(0.8)];
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
gradientLayer.colors = @[(id)[UIColor purpleColor].CGColor, (id)[UIColor greenColor].CGColor, (id)[UIColor orangeColor].CGColor, (id)[UIColor blackColor].CGColor];
[self.view.layer addSublayer:gradientLayer];

3.4 再舉個栗子🌰🌰

當CAGradientLayer + CAShapeLayer + 貝塞爾曲線 會有什麼效果?上代碼~

- (void)setupUI {
    // 貝塞爾曲線
    UIBezierPath *maskPath = [UIBezierPath bezierPath];
    [maskPath moveToPoint:CGPointMake(100, 220)];
    [maskPath addLineToPoint:CGPointMake(200, 150)];
    [maskPath addLineToPoint:CGPointMake(300, 220)];
    [maskPath stroke];

    UIBezierPath *maskBottomPath = [UIBezierPath bezierPath];
    [maskBottomPath moveToPoint:CGPointMake(280, 250)];
    [maskBottomPath addCurveToPoint:CGPointMake(120, 250) controlPoint1:CGPointMake(250, 320) controlPoint2:CGPointMake(150, 320)];
    [maskBottomPath stroke];

    [maskPath appendPath:maskBottomPath];

    // CAShapeLayer
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.view.bounds;
    maskLayer.path = maskPath.CGPath;
    maskLayer.lineWidth = 20;
    maskLayer.strokeColor = UIColorFromRGB(0xF0F5FF).CGColor;
    maskLayer.lineCap = kCALineCapRound;
    maskLayer.lineJoin = kCALineJoinRound;
    maskLayer.fillColor = [UIColor clearColor].CGColor;
    maskLayer.strokeStart = 0;
    maskLayer.strokeEnd = 0;
    [self.view.layer addSublayer:maskLayer];

    // CAGradientLayer
    NSMutableArray *colorArray = [NSMutableArray new];
    for (NSInteger i = 0; i < 6; i++) {
        [colorArray addObject:[self arc4randomColor]];
    }

    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.view.bounds;
    gradientLayer.colors = colorArray;
    gradientLayer.startPoint = CGPointMake(0, 0.5);
    gradientLayer.endPoint = CGPointMake(1, 0.5);
    gradientLayer.mask = maskLayer;
    [self.view.layer addSublayer:gradientLayer];

    // 創建全局併發隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 創建定時器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 設置定時器,每N秒觸發
    int64_t intervalInNanoseconds = (int64_t)(0.3 * NSEC_PER_SEC);
    dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (uint64_t)intervalInNanoseconds, 0);
    // 設置定時器處理事件
    dispatch_source_set_event_handler(timer, ^{
        dispatch_async(dispatch_get_main_queue(), ^{

            if (maskLayer.strokeEnd < 0.6) {
                maskLayer.strokeEnd += 0.4;
            } else if (maskLayer.strokeEnd < 0.8){
                maskLayer.strokeEnd += 0.2;
            } else if (maskLayer.strokeEnd < 1){
                maskLayer.strokeEnd += 0.1;
            } else {
                maskLayer.strokeEnd = 1;
                if (maskLayer.strokeStart < 0.6) {
                    maskLayer.strokeStart += 0.4;
                } else if (maskLayer.strokeStart < 0.8){
                    maskLayer.strokeStart += 0.2;
                } else if (maskLayer.strokeStart < 1){
                    maskLayer.strokeStart += 0.1;
                } else {
                    [colorArray removeObjectAtIndex:0];
                    [colorArray addObject:[self arc4randomColor]];
                    gradientLayer.colors = colorArray;

                    maskLayer.strokeStart = 0;
                    maskLayer.strokeEnd = 0;
                }
            }
        });
    });

    _timer = timer;

    // 開啓定時器
    dispatch_resume(_timer);
}

- (id)arc4randomColor {
    return (id)[UIColor colorWithRed:arc4random()%255/255.f
                               green:arc4random()%255/255.f
                                blue:arc4random()%255/255.f
                               alpha:1].CGColor;
}

運行結果👇

<https://v.qq.com/x/page/l3146iykm06.html

其他layer怎麼使用?貝塞爾曲線 + Layer 還可以組合出更多神奇的反應!感興趣的可以去試試哦~本文僅爲拋磚引玉~~

本文轉載自公衆號貝殼產品技術(ID:beikeTC)。

原文鏈接

貝塞爾曲線在iOS端的繪圖實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章