SwiftNotice源碼詳解

之前用到了一個(gè)比較方便的第三方HUD庫SwiftNotice,一行代碼即可實(shí)現(xiàn)一個(gè)簡潔方便的HUD,最近心血來潮突發(fā)奇想準(zhǔn)備研究研究這個(gè)庫的內(nèi)部實(shí)現(xiàn)原理。在看之前本以為挺簡單的,殊不知麻雀雖小,五臟俱全,涉及到的知識點(diǎn)非常多,看得我這個(gè)小白一頭包。不過經(jīng)過各種查閱資料后,基本原理還是搞清楚了,接下來我將逐步給大家分析下這個(gè)庫是如何一步步地實(shí)現(xiàn)下圖所示的各種功能的(GIF來自于作者的github):

SwiftNotice.gif

從gif圖中我們可以看到作者一共為我們提供了六種HUD類型,還有一個(gè)一鍵清除所有Notice。調(diào)用非常方便,我以noticeOnlyText為例,代碼如下:

self.noticeOnlyText("Only Text")

根據(jù)作者的使用場景介紹:

Pretty easy to use:
In any subclass of UIView, UIScrollView, UIViewController, UITableViewController, UITableViewCell.

我們猜測作者應(yīng)該針對上述UI控件實(shí)現(xiàn)了一個(gè)擴(kuò)展,現(xiàn)在看代碼,證明了我們的猜測是正確的:

extension UIResponder {
    /// wait with your own animated images
    @discardableResult
    func pleaseWaitWithImages(_ imageNames: Array<UIImage>, timeInterval: Int) -> UIWindow{
        return SwiftNotice.wait(imageNames, timeInterval: timeInterval)
    }
    ......
}

作者對UIResponder做了一個(gè)擴(kuò)展,UIResponder是上述控件的基類,具體細(xì)節(jié)就不展開了,有興趣的童鞋可以自行查閱資料了解。

作者在UIResponder的擴(kuò)展中聲明了展示所有種類Notice需要調(diào)用的方法,包括:
pleaseWaitWithImages
noticeTop
noticeSuccess
noticeError
noticeInfo
noticeOnlyText
clearAllNotice
等等等等,這些是作為接口暴露給用戶的,具體的實(shí)現(xiàn)由SwiftNotice這個(gè)類完成,SwiftNotice中對應(yīng)的方法才是真正展示HUD的核心。

因?yàn)榫唧w的實(shí)現(xiàn)都是大同小異的,所以我選了幾個(gè)有代表性的方法來具體分析,分別是:
noticeOnStatusBar:狀態(tài)欄通知;
wait:等待中HUD;
showNoticeWithText:帶文字描述的HUD,包括三種類別:成功、錯(cuò)誤、信息;
hideNotice:隱藏通知或HUD;
clear:清空所有的通知和HUD;
其它的都是類似的。

限于篇幅,我不會分析作者對iOS9之前的系統(tǒng)所做的處理,也不會對其它非核心知識點(diǎn)展開分析,例如:動畫效果、位置大小屬性等等。

在分析方法之前,我們首先看下SwiftNotice類中對應(yīng)的變量:

static var windows = Array<UIWindow!>()
static let rv = UIApplication.shared.keyWindow?.subviews.first as UIView!
static var timer: DispatchSource!
static var timerTimes = 0

windows是一個(gè)UIWindow數(shù)組,每一個(gè)的HUD都建立在一個(gè)UIWindow上。為什么要用UIWindow呢,按照我的理解,每一個(gè)HUD對象的出現(xiàn)都會打斷用戶當(dāng)前的操作,而UIWindow有三個(gè)不同級別的層級,分別是Normal,StatusBar,Alert,層級越高顯示越靠上,通常我們的程序的界面都是處于Normal這個(gè)級別上的,所以我們可以自己定義一個(gè)較高層級的UIWindow,來防止我們的HUD被其他UI控件遮擋;

rv是一個(gè)keyWindow的第一個(gè)子View,這里需要先解釋下keyWindow,在我們的程序中,可以有很多個(gè)window,但是同一時(shí)刻只能有一個(gè)keyWindow,舉個(gè)例子:一個(gè)程序啟動,此時(shí)只有一個(gè)默認(rèn)的window,級別為Normal,這個(gè)window同時(shí)也是keyWindow,然后我們自己新建一個(gè)window并將其顯示在屏幕上,級別設(shè)置成更高的StatusBar,注意,此時(shí)的keyWindow就變成了我們新建的window,默認(rèn)的window變?yōu)榉?code>keyWindow。換句話說,rv是獲取當(dāng)前級別最高的window的第一個(gè)子View;

timer是一個(gè)計(jì)時(shí)器,所有的HUD和通知的自動消失都是通過計(jì)時(shí)器完成的,作者并沒有使用NSTimer而是使用了DispatchSource,關(guān)于DispatchSource的使用和優(yōu)點(diǎn)可以參考這篇文章:打造一個(gè)優(yōu)雅的Timer;

timerTimes用來計(jì)數(shù),稍后在wait方法中具體分析;

我們首先分析第一個(gè)方法,noticeOnStatusBar,直接上代碼:

static func noticeOnStatusBar(_ text: String, autoClear: Bool, autoClearTime: Int) -> UIWindow{
        let frame = UIApplication.shared.statusBarFrame
        let window = UIWindow()
        window.backgroundColor = UIColor.clear
        let view = UIView()
        view.backgroundColor = UIColor(red: 0x6a/0x100, green: 0xb4/0x100, blue: 0x9f/0x100, alpha: 1)
        
        let label = UILabel(frame: frame)
        label.textAlignment = NSTextAlignment.center
        label.font = UIFont.systemFont(ofSize: 12)
        label.textColor = UIColor.white
        label.text = text
        view.addSubview(label)
        
        window.frame = frame
        view.frame = frame
        
        if let version = Double(UIDevice.current.systemVersion),
            version < 9.0 {
            // change center
            var array = [UIScreen.main.bounds.width, UIScreen.main.bounds.height]
            array = array.sorted(by: <)
            let screenWidth = array[0]
            let screenHeight = array[1]
            let x = [0, screenWidth/2, screenWidth/2, 10, screenWidth-10][UIApplication.shared.statusBarOrientation.hashValue] as CGFloat
            let y = [0, 10, screenHeight-10, screenHeight/2, screenHeight/2][UIApplication.shared.statusBarOrientation.hashValue] as CGFloat
            window.center = CGPoint(x: x, y: y)
            
            // change direction
            window.transform = CGAffineTransform(rotationAngle: CGFloat(degree * Double.pi / 180))
        }
        
        window.windowLevel = UIWindowLevelStatusBar //UIWindow的層級
        window.isHidden = false
        window.addSubview(view)
        windows.append(window)
      
        var origPoint = view.frame.origin //確定初始位置
        origPoint.y = -(view.frame.size.height)
        let destPoint = view.frame.origin //確定最終位置
        view.tag = sn_topBar
      
        view.frame = CGRect(origin: origPoint, size: view.frame.size)
        UIView.animate(withDuration: 0.3, animations: {
          view.frame = CGRect(origin: destPoint, size: view.frame.size)
        }, completion: { b in //閉包判斷是否需要自動消失
          if autoClear {
              let selector = #selector(SwiftNotice.hideNotice(_:))
              self.perform(selector, with: window, afterDelay: TimeInterval(autoClearTime))
          }
        })
      return window
    }

上述代碼可以分為兩個(gè)部分:確定位置和大?。▽傩裕?、顯示和消失(動作)(iOS9以下處理的部分不考慮)。

window本身不用做展示,顯示層級依次為:window->view->label,需要確定他們的位置和大小以及一些其它的屬性。需要注意的是設(shè)置window的層級以及將window添加到windows數(shù)組中。因?yàn)樵撏ㄖ詣赢嫷姆绞匠霈F(xiàn),所以需要確定動畫初始位置和動畫結(jié)束位置,動畫閉包中判斷是否需要自動消失(根據(jù)傳的參數(shù)),如果需要的話調(diào)用hideNotice方法。這里我們調(diào)用了一個(gè)perform方法,注意注意,敲黑板劃重點(diǎn)了,這是iOS中延遲執(zhí)行的一個(gè)方法,引用這位博主所說:

當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實(shí)際上其內(nèi)部會創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒有 RunLoop,則這個(gè)方法會失效。

RunLoop我目前也不是特別了解,有興趣的讀者可以自行查閱資料或者查看上面我引用的那個(gè)博主的博客。

接下來我們分析hideNotice這個(gè)方法,代碼先上:

static func hideNotice(_ sender: AnyObject) {
        if let window = sender as? UIWindow {
          
          if let v = window.subviews.first { //獲取window的第一個(gè)子view并將它消失(動畫效果)
            UIView.animate(withDuration: 0.2, animations: {
              
              if v.tag == sn_topBar { //再次確定view的tag(確定是否是StatusNotice)
                v.frame = CGRect(x: 0, y: -v.frame.height, width: v.frame.width, height: v.frame.height) //移出屏幕
              }
              v.alpha = 0 //變?yōu)橥该?            }, completion: { b in
              
              if let index = windows.index(where: { (item) -> Bool in //這里用到了Swift數(shù)組的index方法,參數(shù)是一個(gè)閉包,只有參數(shù)和window相同時(shí)返回真,找到window在windows中的下標(biāo)
                  return item == window
              }) {
                  windows.remove(at: index) //從視圖數(shù)組中移除視圖
              }
            })
          }
          
        }
    }

這個(gè)方法將StatusBar通知和HUD的隱藏寫在了一起,首先確定要移除的window,然后獲取到window的子View,在執(zhí)行消失動畫的時(shí)候判斷該View是不是StatusBar通知View,對應(yīng)不同的消失方法,在動畫方法的閉包中獲取到要移除的windowwindows數(shù)組中的下標(biāo),然后將其從數(shù)組中移除。這里多說幾句,注意下面這個(gè)方法:

if let index = windows.index(where: { (item) -> Bool in //這里用到了Swift數(shù)組的index方法,參數(shù)是一個(gè)閉包,只有參數(shù)和window相同時(shí)返回真,找到window在windows中的下標(biāo)
    return item == window
}) {
    windows.remove(at: index) //從視圖數(shù)組中移除視圖
}

注意我寫的注釋:這里用到了Swift數(shù)組的index方法,參數(shù)是一個(gè)閉包,只有參數(shù)和window相同時(shí)返回真,找到window在windows中的下標(biāo)。

現(xiàn)在我們再來分析下wait方法,老套路,先看代碼:

static func wait(_ imageNames: Array<UIImage> = Array<UIImage>(), timeInterval: Int = 0) -> UIWindow { //參數(shù)為可選參數(shù),調(diào)用時(shí)沒有參數(shù)即使用默認(rèn)參數(shù)
        let frame = CGRect(x: 0, y: 0, width: 78, height: 78)
        let window = UIWindow()
        window.backgroundColor = UIColor.clear
        let mainView = UIView()
        mainView.layer.cornerRadius = 12
        mainView.backgroundColor = UIColor(red:0, green:0, blue:0, alpha: 0.8)
        
        if imageNames.count > 0 {
            if imageNames.count > timerTimes {
                let iv = UIImageView(frame: frame)
                iv.image = imageNames.first!
                iv.contentMode = UIViewContentMode.scaleAspectFit
                mainView.addSubview(iv)
                timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: UInt(0)), queue: DispatchQueue.main) as! DispatchSource
                timer.scheduleRepeating(deadline: DispatchTime.now(), interval: DispatchTimeInterval.milliseconds(timeInterval))
                timer.setEventHandler(handler: { () -> Void in
                    let name = imageNames[timerTimes % imageNames.count]
                    iv.image = name
                    timerTimes += 1
                })
                timer.resume()
            }
        } else {
            let ai = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.whiteLarge)
            ai.frame = CGRect(x: 21, y: 21, width: 36, height: 36)
            ai.startAnimating()
            mainView.addSubview(ai)
        }
        
        window.frame = frame
        mainView.frame = frame
        window.center = rv!.center
        
        if let version = Double(UIDevice.current.systemVersion),
            version < 9.0 {
            // change center
            window.center = getRealCenter()
            // change direction
            window.transform = CGAffineTransform(rotationAngle: CGFloat(degree * Double.pi / 180))
        }
        
        window.windowLevel = UIWindowLevelAlert
        window.isHidden = false
        window.addSubview(mainView)
        windows.append(window)
      
        mainView.alpha = 0.0
        UIView.animate(withDuration: 0.2, animations: {
          mainView.alpha = 1
        })
        return window
    }

從代碼中可以看出視圖層級是這樣的:
window->mainView->imageView,基本類似于之前的層級。
唯獨(dú)需要注意的地方是image部分,作者已經(jīng)為我們自定義wait時(shí)的動畫做好了準(zhǔn)備工作,我們只要將動畫分成幾個(gè)不同的image,放到imageNames這個(gè)數(shù)組中。那么具體是如何實(shí)現(xiàn)image依次出現(xiàn)并循環(huán)播放的呢?這里用到了之前定義的timer。下面是核心代碼:

timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: UInt(0)), queue: DispatchQueue.main) as! DispatchSource
timer.scheduleRepeating(deadline: DispatchTime.now(), interval: DispatchTimeInterval.milliseconds(timeInterval))
timer.setEventHandler(handler: { () -> Void in
    let name = imageNames[timerTimes % imageNames.count]
    iv.image = name
    timerTimes += 1
})
timer.resume()

在分析代碼之前,我首先引用上文中提到的《打造一個(gè)優(yōu)雅的Timer》中的一段話:

一般來說,DispatchSource的使用步驟就是:創(chuàng)建一個(gè)想要監(jiān)聽的事件類型對應(yīng)的dispatch source,然后給這個(gè)source指定一個(gè)閉包,指定一個(gè)Dispatch Queue。當(dāng)source監(jiān)聽到相應(yīng)的事件時(shí),就會將該閉包自動加到queue中執(zhí)行。

現(xiàn)在我們分析代碼:
第一行,作者創(chuàng)建了一個(gè)Dispatch Source,對應(yīng)Timer Dispatch Source類型,并指定了DispatchQueue為主隊(duì)列;
第二行,作者設(shè)置了Source的監(jiān)聽事件為重復(fù)執(zhí)行;
第三行,作者為該Source指定了一個(gè)閉包,為了實(shí)現(xiàn)循環(huán)播放圖片形成動畫,作者用計(jì)數(shù)變量timerTimes和image數(shù)組個(gè)數(shù)取余,得到的結(jié)果作為image數(shù)組的下標(biāo),非常巧妙;
最后一行,不要忘了啟動計(jì)時(shí)器。

如果沒有自定義的圖片作為等待時(shí)的展示,就直接使用默認(rèn)的UIActivityIndicatorView,這里不再細(xì)說,代碼寫得非常清楚。

下面我們要分析的是showNoticeWithText這個(gè)方法,和之前的wait方法不同的是,showNoticeWithText不需要用戶提供圖片,圖片由庫本身提供,具體如何提供則是由另一個(gè)類來完成,SwiftNoticeSDK。因此,SwiftNoticeSDK是我們著重分析的對象。

先看代碼:

class SwiftNoticeSDK {
    struct Cache {
        static var imageOfCheckmark: UIImage?
        static var imageOfCross: UIImage?
        static var imageOfInfo: UIImage?
    }
    class func draw(_ type: NoticeType) {
        let checkmarkShapePath = UIBezierPath()
        
        /*
        畫圓參數(shù)解釋:
        1.center: CGPoint  中心點(diǎn)坐標(biāo)
        2.radius: CGFloat  半徑
        3.startAngle: CGFloat 起始點(diǎn)所在弧度
        4.endAngle: CGFloat   結(jié)束點(diǎn)所在弧度
        5.clockwise: Bool     是否順時(shí)針繪制
        7.畫圓時(shí),沒有坐標(biāo)這個(gè)概念,根據(jù)弧度來定位起始點(diǎn)和結(jié)束點(diǎn)位置。M_PI即是圓周率。畫半圓即(0,M_PI),代表0到180度。全圓則是(0,M_PI*2),代表0到360度
        */ 
        // draw circle
        checkmarkShapePath.move(to: CGPoint(x: 36, y: 18))
        checkmarkShapePath.addArc(withCenter: CGPoint(x: 18, y: 18), radius: 17.5, startAngle: 0, endAngle: CGFloat(Double.pi*2), clockwise: true)
        checkmarkShapePath.close()
        
        switch type {
        case .success: // draw checkmark
            checkmarkShapePath.move(to: CGPoint(x: 10, y: 18))
            checkmarkShapePath.addLine(to: CGPoint(x: 16, y: 24))
            checkmarkShapePath.addLine(to: CGPoint(x: 27, y: 13))
            checkmarkShapePath.move(to: CGPoint(x: 10, y: 18))
            checkmarkShapePath.close()
        case .error: // draw X
            checkmarkShapePath.move(to: CGPoint(x: 10, y: 10))
            checkmarkShapePath.addLine(to: CGPoint(x: 26, y: 26))
            checkmarkShapePath.move(to: CGPoint(x: 10, y: 26))
            checkmarkShapePath.addLine(to: CGPoint(x: 26, y: 10))
            checkmarkShapePath.move(to: CGPoint(x: 10, y: 10))
            checkmarkShapePath.close()
        case .info:
            checkmarkShapePath.move(to: CGPoint(x: 18, y: 6))
            checkmarkShapePath.addLine(to: CGPoint(x: 18, y: 22))
            checkmarkShapePath.move(to: CGPoint(x: 18, y: 6))
            checkmarkShapePath.close()
            
            UIColor.white.setStroke()
            checkmarkShapePath.stroke()
            
            let checkmarkShapePath = UIBezierPath()
            checkmarkShapePath.move(to: CGPoint(x: 18, y: 27))
            checkmarkShapePath.addArc(withCenter: CGPoint(x: 18, y: 27), radius: 1, startAngle: 0, endAngle: CGFloat(Double.pi*2), clockwise: true)
            checkmarkShapePath.close()
            
            UIColor.white.setFill()
            checkmarkShapePath.fill()
        }
        
        UIColor.white.setStroke()
        checkmarkShapePath.stroke()
    }
    class var imageOfCheckmark: UIImage {
        if (Cache.imageOfCheckmark != nil) {
            return Cache.imageOfCheckmark!
        }
        UIGraphicsBeginImageContextWithOptions(CGSize(width: 36, height: 36), false, 0)
        
        SwiftNoticeSDK.draw(NoticeType.success)
        
        Cache.imageOfCheckmark = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return Cache.imageOfCheckmark!
    }
    class var imageOfCross: UIImage {
        if (Cache.imageOfCross != nil) {
            return Cache.imageOfCross!
        }
        UIGraphicsBeginImageContextWithOptions(CGSize(width: 36, height: 36), false, 0)
        
        SwiftNoticeSDK.draw(NoticeType.error)
        
        Cache.imageOfCross = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return Cache.imageOfCross!
    }
    class var imageOfInfo: UIImage {
        if (Cache.imageOfInfo != nil) {
            return Cache.imageOfInfo!
        }
        UIGraphicsBeginImageContextWithOptions(CGSize(width: 36, height: 36), false, 0)
        
        SwiftNoticeSDK.draw(NoticeType.info)
        
        Cache.imageOfInfo = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return Cache.imageOfInfo!
    }
}

SwiftNoticeSDK主要由三個(gè)部分組成,分別是結(jié)構(gòu)體Cache,繪制方法draw和三個(gè)通過懶加載實(shí)現(xiàn)的UIImage對象,其中核心是draw方法,至此我們知道,SwiftNoticeSDK中并不包含圖像,它直接繪制了三個(gè)圖像。那么它是如何繪制的呢?這里作者使用了一個(gè)狂拽酷炫吊炸天的類:UIBezierPath。使用UIBezierPath可以創(chuàng)建基于矢量的路徑。使用此類可以定義簡單的形狀,如橢圓、矩形或者有多個(gè)直線和曲線段組成的形狀等。它的部分屬性如下:

moveToPoint: //設(shè)置起始點(diǎn)
addLineToPoint: //從上一點(diǎn)連接一條線到本次指定的點(diǎn)
closePath() //閉合路徑,把起始點(diǎn)和終點(diǎn)連接起來
appendPath: //多條路徑合并
removeAllPoints() //刪除所有點(diǎn)和線
lineWidth //路徑寬度
lineCapStyle //端點(diǎn)樣式(枚舉)
lineJoinStyle //連接點(diǎn)樣式(枚舉)
//下面這幾個(gè)屬性要用在UIView中重寫drawRect:方法中使用才有效,否則不會出現(xiàn)效果
UIColor.redColor().setStroke() //設(shè)置路徑顏色(不常用)
stroke()渲染路徑
UIColor.redColor().setFill() //設(shè)置填充顏色(不常用)
fill()渲染填充部分

通過上述屬性我們可以很容易理解各種圖形的繪制過程。但是我們還需要將繪制好的內(nèi)容賦值給對應(yīng)的圖像,在draw方法調(diào)用之前調(diào)用UIGraphicsBeginImageContextWithOptions方法,按照蘋果官方文檔的解釋,UIGraphicsBeginImageContextWithOptions方法使用指定的選項(xiàng)創(chuàng)建一個(gè)基于位圖的圖形上下文。當(dāng)此函數(shù)創(chuàng)建的上下文是當(dāng)前上下文,就可以調(diào)用UIGraphicsGetImageFromCurrentImageContext函數(shù)根據(jù)上下文的當(dāng)前內(nèi)容檢索圖像對象,因此我們在draw方法之后調(diào)用UIGraphicsGetImageFromCurrentImageContext函數(shù)得到UIImage圖像并賦值給對應(yīng)的值。當(dāng)然了,最后需要從棧中刪除頂部當(dāng)前基于位圖的圖形上下文,使用UIGraphicsEndImageContext方法。

最后我們來分析清除所有屏幕上的HUD和通知的clear方法,代碼如下:

static func clear() {
        self.cancelPreviousPerformRequests(withTarget: self)
        if let _ = timer {
            timer.cancel()
            timer = nil
            timerTimes = 0
        }
        windows.removeAll(keepingCapacity: false)
    }

總體思路比較簡單,就是將windows數(shù)組中的window全部移除,同時(shí)完成一些收尾工作:如果perform方法還未執(zhí)行則取消執(zhí)行,計(jì)時(shí)器取消并置為nil。

至此,SwiftNotice的核心代碼,包括實(shí)現(xiàn)思路就都過了一遍。在查看并研究源碼的這兩天,我也查漏補(bǔ)缺,學(xué)習(xí)了不少新知識,但是限于我個(gè)人的知識面,可能一些分析并不是十分完整,也歡迎大家給我提出寶貴意見。在我看來,知其然,更要知其所以然,在我們使用第三方庫的時(shí)候,不光要會用,也應(yīng)該知道作者是如何實(shí)現(xiàn)它的,然后自己再實(shí)現(xiàn)一次,從易到難,對自己的編程能力一定會有很大的提升!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,979評論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,208評論 4 61
  • As for me , writing in English is an arduous process , ...
    Y型思考閱讀 636評論 3 2
  • 有很多小伙伴知道我買了kindle paperwhite的電紙書,紛紛也資訊我如何選擇電紙書。然而,我這部小K是兩...
    zerofool閱讀 2,557評論 3 1
  • 明天就出成績了,今天的我陷入了一種焦灼的情緒里。想要到處求安慰,又因?yàn)榭剂颂啻?,不能理解的人會有些風(fēng)言風(fēng)語...
    庭生閱讀 214評論 0 0

友情鏈接更多精彩內(nèi)容