iOS開發(fā) 氣泡和sheet

Demo 地址

需實(shí)現(xiàn)效果:

氣泡.png

我們在日常開發(fā)中經(jīng)常會用到這樣的氣泡控件,以前都是直接在GitHub里面找一個(gè)典勇,最近有時(shí)間就想著自己寫一個(gè)羹奉。

思路&實(shí)現(xiàn)路線

1.獲取必要參數(shù)

首先就是頂部的三角形,它的頂點(diǎn)是在我們點(diǎn)擊的view中心點(diǎn)的下方煤辨,所以要先拿到點(diǎn)擊的viewframe裳涛,因此我們就需要一個(gè)這樣的必要參數(shù):pointView,把這個(gè)參數(shù)寫到init方法里面,參數(shù):

    ///lineHeight : 每一行的高度众辨, titles:標(biāo)題端三,image:圖片,要與titles數(shù)量想的鹃彻,target:響應(yīng)時(shí)間郊闯,需要?jiǎng)?chuàng)建一個(gè)Target類型,bubbleStyle:0dark,黑暗色团赁,1light育拨,明亮色
    @objc init(lineHeight: CGFloat = 44, titles: [String], images:[Any]? = nil, target: Target?=nil, bubbleStyle:KLNBubbleStyle = .dark, sender: NSObject) {

        self.lineHeight = lineHeight
        self.titles = titles
        
        if let images = images, images.count > 0 {
            self.images = images
            if titles.count != images.count {
                _="圖片和文字的數(shù)量必須要相等!"
                abort()
            }
        }
        
        if let view = sender as? UIView {
            self.pointView = view         
        }else if let view = sender.value(forKey: "view") as? UIView {
         //sender如果是UIBarButtonItem的時(shí)候
            self.pointView = view
        }
        self.bubbleStyle = bubbleStyle

        let alpha:CGFloat = 0.98
        if bubbleStyle == .dark {
            kTextColor = UIColor.init(hex: "#F7F9Fb").withAlphaComponent(alpha)
            kBackColor = UIColor.init(hex: "#1F1F1F").withAlphaComponent(alpha)
        }else{
            kTextColor = UIColor.init(hex: "#1F1F1F").withAlphaComponent(alpha)
            kBackColor = UIColor.init(hex: "#F7F9Fb").withAlphaComponent(alpha)
        }
        super.init(nibName: nil, bundle: nil)
        self.modalPresentationStyle = .overCurrentContext
    }

以上參數(shù)需要解釋的是target欢摄,這是之前我的一個(gè)大佬同事教我的熬丧,目的是使用perform這個(gè)方法來替代block,降低因使用block而引起循環(huán)引用的幾率怀挠。當(dāng)然析蝴,也并不是適用替換所有的使用block的場景,我貼一下绿淋,可選擇使用闷畸。

@objc public class Target: NSObject {
    @objc weak var target : NSObject?
    @objc var selector : Selector?
    
    @objc func perform(object: Any!) {
        target?.perform(selector, with: object)
    }
    
    @objc func doAction(object: Any!) {
        target?.perform(selector, with: object)
    }
    
    @objc func perform(object1: Any!, object2: Any!) {
        target?.perform(selector, with: object1, with: object2)
    }
    
    @objc init(target:NSObject?, selector:Selector?) {
        super.init()
        self.selector = selector
        self.target = target
    }
    
}

還有pointView,你點(diǎn)的是誰不重要吞滞,重要的是你把誰當(dāng)做pointView傳過來佑菩,就以誰為標(biāo)準(zhǔn)來顯示。

init方法里面獲取到了我們需要的所有必要參數(shù)裁赠,需要顯示的標(biāo)題數(shù)組:titles殿漠,點(diǎn)擊的view:pointView。拿到這兩個(gè)參數(shù)我們就可以確定氣泡的具體位置了组贺。至于其他的參數(shù)都是可有可無凸舵,直接給個(gè)默認(rèn)值就行。當(dāng)然失尖,暴露出來給調(diào)用者選擇更好啊奄。

2.準(zhǔn)備畫圖

拿到數(shù)組以后,我們首先要做的要看一下這個(gè)數(shù)組中最長的字符串的長度是多少掀潮。因?yàn)槲覀兊倪@個(gè)氣泡肯定是要按照最長的長度來畫菇夸。
于是我選擇循環(huán)來拿到最大長度,并將兩邊留出8像素的空白,如果有圖片的話仪吧,再加上24給圖片留位置:

        var maxWidth:CGFloat = 0
        for text in titles {
            let width = KGetLabWidth(labelStr: text, font: font, height: lineHeight)
            maxWidth = maxWidth > width ? maxWidth:width
        }
        
        maxWidth = maxWidth + 16 + (images == nil ? 0:24)

然后確定頂部三角形的高度

    //三角的高度
    fileprivate var angleHeight:CGFloat = 12

拿到pointView的位置

        //這個(gè)參數(shù)作用是計(jì)算pointView底部距離屏幕底部的高度是否夠用
        var kBottomSapce:CGFloat = 0
        
        var frame = CGRect.zero
        if let window = UIApplication.shared.windows.first {
            frame = pointView.convert(pointView.bounds, to: window)
            kBottomSapce = window.frame.size.height - frame.origin.y
        }

至此庄新,我們拿到了pointView的位置、三角形的高度和titles的數(shù)量薯鼠,那就可以直接確定氣泡的frame了:

        bubbleView = UIView.init(frame: CGRect.init(x: 0, y: frame.origin.y + frame.size.height, width: maxWidth, height: CGFloat(titles.count) * lineHeight + angleHeight))
        self.view.addSubview(bubbleView)

        //左右間隙不能太小
        let centerX = frame.midX
        //氣泡view和pointView垂直對齊
        bubbleView.center.x = centerX

        //左右間隙不能太小,如果pointView太靠邊的話择诈,我們也要適當(dāng)調(diào)整一下位置
        if centerX + maxWidth/2  > UIScreen.width {
            bubbleView.ln_right = UIScreen.width - 5
        }
        if centerX - maxWidth/2  < 0 {
            bubbleView.ln_x = 5
        }

然后在bubbleView里面添加三角形視圖和下面的列表:

        let angleView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: bubbleView.ln_width, height: angleHeight))
        bubbleView.addSubview(angleView)
                
        let showView = UIView.init(frame: CGRect.init(x: 0, y: angleHeight, width: bubbleView.ln_width, height: bubbleView.ln_height - angleHeight))
        showView.ln_cornerRadius = 4
        showView.backgroundColor = kBackColor
        bubbleView.addSubview(showView)

然后開始畫三角形:


        //以視圖的中心點(diǎn)為原點(diǎn)找位置
        //就是以pointView的center.x為原點(diǎn),獲取x軸坐標(biāo)點(diǎn)出皇,并適當(dāng)調(diào)整位置羞芍,不要太靠邊,可參照下圖理解其作用
        func getX(_ value: CGFloat) -> CGFloat {
            
            var x = centerX  - bubbleView.ln_x
            x = x > bubbleView.ln_width - 14 ? bubbleView.ln_width - 14:x
            x = x < 14 ? 14:x
            
            return x  + value
        }

        let bezir = UIBezierPath.init()
        //點(diǎn)擊的視圖下方間距是否足夠氣泡
        let isBottomSpaceEnough = kBottomSapce >= bubbleView.ln_height
        if !isBottomSpaceEnough {
            //下方位置不夠時(shí)郊艘,氣泡的位置也要變一下荷科,箭頭需要反過來唯咬,列表就在上面了
            bubbleView.ln_y = frame.origin.y - bubbleView.ln_height
            angleView.ln_y = bubbleView.ln_height - angleHeight
            showView.ln_y = 0
            //箭頭向下
            bezir.move(to: CGPoint.init(x: getX(-10), y: 0))
            bezir.addLine(to: CGPoint.init(x: getX(0), y: 7.5))
            bezir.addLine(to: CGPoint.init(x: getX(10), y: 0))
            bezir.addLine(to: CGPoint.init(x: getX(-10), y: 0))
        }else{
            //箭頭向上
            bezir.move(to: CGPoint.init(x: getX(-10), y: angleHeight))
            bezir.addLine(to: CGPoint.init(x: getX(0), y: 3.5))
            bezir.addLine(to: CGPoint.init(x: getX(10), y: angleHeight))
            bezir.addLine(to: CGPoint.init(x: getX(-10), y: angleHeight))
        }
        
        let shape = CAShapeLayer.init()
        shape.lineWidth = 1
        shape.fillColor = kBackColor.cgColor
        shape.cornerRadius = 3
        shape.path = bezir.cgPath
        angleView.layer.addSublayer(shape)
當(dāng)pointView太過靠邊的時(shí)候,箭頭適當(dāng)往內(nèi)側(cè)移動(dòng).png

箭頭畫完了畏浆,開始寫列表了胆胰,我就直接用了一個(gè)循環(huán):

        for index in 0..<titles.count {
            let buttonItem = UIButton.init(frame: CGRect.init(x: 0, y: CGFloat(index)*lineHeight, width: maxWidth, height: lineHeight))
            buttonItem.setTitle(titles[index], for: .normal)
           
            if images != nil {
                if let string = images?[index] as? String {
                    if string.hasPrefix("http") {
                        //換上你喜歡的加載圖片的方式
                        //buttonItem.kf.setImage(with: URL.init(string: string), for: .normal, placeholder: UIImage.init(named: "placeholder_1"))
                    }else{
                        buttonItem.setImage(UIImage.init(named: string), for: .normal)
                    }
                }else if let image = images?[index] as? UIImage {
                    buttonItem.setImage(image, for: .normal)
                }
            }
            buttonItem.titleLabel?.font = font
            buttonItem.setTitleColor(kTextColor, for: .normal)
            buttonItem.addTarget(self, action: #selector(chooseTarget(sender:)), for: .touchUpInside)
            buttonItem.tag = 100+index
            showView.addSubview(buttonItem)
            
            if index == titles.count - 1 {
                break
            }
            let bottomLine = UIView.init(frame: CGRect.init(x: 4, y: buttonItem.ln_height-1, width: buttonItem.ln_width - 8, height: 0.5))
            bottomLine.backgroundColor = kTextColor
            buttonItem.addSubview(bottomLine)
        }

全部文件代碼


import UIKit
import LNTools_fyh

@objc public enum KLNBubbleStyle : Int {
    case dark = 0
    case light
}

class BubbleViewController: UIViewController {
    @objc public var target : Target?

    @objc public var bubbleStyle = KLNBubbleStyle.dark
    //每行的高度
    fileprivate var lineHeight:CGFloat = 44
    //title
    fileprivate var titles:[String] = []
    //圖片image
    fileprivate var images:[Any]?
    //點(diǎn)擊到的view
    fileprivate var pointView:UIView!
    
    //展示整個(gè)氣泡的父容器
    fileprivate var bubbleView : UIView!
    //字體大小
    var font = UIFont.systemFont(ofSize: 16)
    //三角的高度
    fileprivate var angleHeight:CGFloat = 12
    
    //文字顏色
    private var kTextColor = UIColor.black.withAlphaComponent(0.95)
    //背景顏色
    private var kBackColor = UIColor.white.withAlphaComponent(0.95)

    public typealias LNDidSelectBlock = (_ title:String, _ index:Int) -> Void
    fileprivate var didSelect:LNDidSelectBlock? = nil
    public func didSelectAction(callback:@escaping LNDidSelectBlock) {
        self.didSelect = callback
    }
    
    ///lineHeight : 每一行的高度, titles:標(biāo)題刻获,image:圖片蜀涨,要與titles數(shù)量想的,target:響應(yīng)時(shí)間将鸵,需要?jiǎng)?chuàng)建一個(gè)Target類型勉盅,bubbleStyle:0dark佑颇,黑暗色顶掉,1light,明亮色
    @objc init(lineHeight: CGFloat = 44, titles: [String], images:[Any]? = nil, target: Target?=nil, bubbleStyle:KLNBubbleStyle = .dark, sender: NSObject) {

        self.lineHeight = lineHeight
        self.titles = titles
        
        if let images = images, images.count > 0 {
            self.images = images
            if titles.count != images.count {
                _="圖片和文字的數(shù)量必須要相等挑胸!"
                abort()
            }
        }
        
        if let view = sender as? UIView {
            self.pointView = view
        }else if let view = sender.value(forKey: "view") as? UIView {
            self.pointView = view
        }
        self.bubbleStyle = bubbleStyle
        
        let alpha:CGFloat = 0.98
        if bubbleStyle == .dark {
            kTextColor = UIColor.init(hex: "#F7F9Fb").withAlphaComponent(alpha)
            kBackColor = UIColor.init(hex: "#1F1F1F").withAlphaComponent(alpha)
        }else{
            kTextColor = UIColor.init(hex: "#1F1F1F").withAlphaComponent(alpha)
            kBackColor = UIColor.init(hex: "#F7F9Fb").withAlphaComponent(alpha)
        }

        super.init(nibName: nil, bundle: nil)
        self.modalPresentationStyle = .overCurrentContext
    }

    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = UIColor.black.withAlphaComponent(0.11)
        configSubViews()
    }
    
    
    fileprivate func configSubViews() {
        
        var maxWidth:CGFloat = 0
        for text in titles {
            let width = KGetLabWidth(labelStr: text, font: font, height: lineHeight)
            maxWidth = maxWidth > width ? maxWidth:width
        }
        
        maxWidth = maxWidth + 16 + (images == nil ? 0:24)
        
        var kBottomSapce:CGFloat = 0
        
        var frame = CGRect.zero
        if let window = UIApplication.shared.windows.first {
            frame = pointView.convert(pointView.bounds, to: window)
            kBottomSapce = window.frame.size.height - frame.origin.y
        }
        
        bubbleView = UIView.init(frame: CGRect.init(x: 0, y: frame.origin.y + frame.size.height, width: maxWidth, height: CGFloat(titles.count) * lineHeight + angleHeight))
        self.view.addSubview(bubbleView)
        
        let centerX = frame.midX
        //氣泡view和pointView垂直對齊
        bubbleView.center.x = centerX
        
        //左右間隙不能太小,如果pointView太靠邊的話痒筒,我們也要適當(dāng)調(diào)整一下位置
        if centerX + maxWidth/2  > UIScreen.width {
            bubbleView.ln_right = UIScreen.width - 5
        }
        if centerX - maxWidth/2  < 0 {
            bubbleView.ln_x = 5
        }
        
        let angleView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: bubbleView.ln_width, height: angleHeight))
        bubbleView.addSubview(angleView)
                
        let showView = UIView.init(frame: CGRect.init(x: 0, y: angleHeight, width: bubbleView.ln_width, height: bubbleView.ln_height - angleHeight))
        showView.ln_cornerRadius = 4
        showView.backgroundColor = kBackColor
        bubbleView.addSubview(showView)
        
        //以視圖的中心點(diǎn)為原點(diǎn)找位置
        func getX(_ value: CGFloat) -> CGFloat {
            
            var x = centerX  - bubbleView.ln_x
            x = x > bubbleView.ln_width - 14 ? bubbleView.ln_width - 14:x
            x = x < 14 ? 14:x
            
            return x  + value
        }
        
        let bezir = UIBezierPath.init()
        //點(diǎn)擊的視圖下方間距是否足夠顯示氣泡
        let isBottomSpaceEnough = kBottomSapce >= bubbleView.ln_height
        if !isBottomSpaceEnough {
            bubbleView.ln_y = frame.origin.y - bubbleView.ln_height
            angleView.ln_y = bubbleView.ln_height - angleHeight
            showView.ln_y = 0
            //箭頭向下
            bezir.move(to: CGPoint.init(x: getX(-10), y: 0))
            bezir.addLine(to: CGPoint.init(x: getX(0), y: 7.5))
            bezir.addLine(to: CGPoint.init(x: getX(10), y: 0))
            bezir.addLine(to: CGPoint.init(x: getX(-10), y: 0))
        }else{
            //箭頭向上
            bezir.move(to: CGPoint.init(x: getX(-10), y: angleHeight))
            bezir.addLine(to: CGPoint.init(x: getX(0), y: 3.5))
            bezir.addLine(to: CGPoint.init(x: getX(10), y: angleHeight))
            bezir.addLine(to: CGPoint.init(x: getX(-10), y: angleHeight))
        }
        
        let shape = CAShapeLayer.init()
        shape.lineWidth = 1
        shape.fillColor = kBackColor.cgColor
        shape.cornerRadius = 3
        shape.path = bezir.cgPath
        angleView.layer.addSublayer(shape)
        
        for index in 0..<titles.count {
            let buttonItem = UIButton.init(frame: CGRect.init(x: 0, y: CGFloat(index)*lineHeight, width: maxWidth, height: lineHeight))
            buttonItem.setTitle(titles[index], for: .normal)
           
            if images != nil {
                if let string = images?[index] as? String {
                    if string.hasPrefix("http") {
                        //換上你喜歡的加載圖片的方式
//                        buttonItem.kf.setImage(with: URL.init(string: string), for: .normal, placeholder: UIImage.init(named: "placeholder_1"))
                    }else{
                        buttonItem.setImage(UIImage.init(named: string), for: .normal)
                    }
                }else if let image = images?[index] as? UIImage {
                    buttonItem.setImage(image, for: .normal)
                }
            }
            buttonItem.titleLabel?.font = font
            buttonItem.setTitleColor(kTextColor, for: .normal)
            buttonItem.addTarget(self, action: #selector(chooseTarget(sender:)), for: .touchUpInside)
            buttonItem.tag = 100+index
            showView.addSubview(buttonItem)
            
            if index == titles.count - 1 {
                break
            }
            let bottomLine = UIView.init(frame: CGRect.init(x: 4, y: buttonItem.ln_height-1, width: buttonItem.ln_width - 8, height: 0.5))
            bottomLine.backgroundColor = kTextColor
            buttonItem.addSubview(bottomLine)
        }
    }
    
    
    @objc func chooseTarget(sender: UIButton) {
        
        let index = sender.tag-100
        
        target?.perform(object1: titles[index], object2: "\(index)")
        didSelect?(titles[index],index)
        
        UIView.animate(withDuration: 0.15) {
            self.bubbleView.alpha = 0
        }
        self.dismiss(animated: false, completion: nil)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        UIView.animate(withDuration: 0.15) {
            self.bubbleView.alpha = 0
        }
        self.dismiss(animated: false, completion: nil)
    }

    //MARK:獲取字符串的寬度的封裝
    func KGetLabWidth(labelStr:String,font:UIFont,height:CGFloat) -> CGFloat {
        
        let statusLabelText: NSString = labelStr as NSString
        
        let size = CGSize(width: 900, height: height)
        
        let dic = NSDictionary(object: font, forKey: NSAttributedString.Key.font as NSCopying)
        
        let strSize = statusLabelText.boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: dic as? [NSAttributedString.Key : Any], context:nil).size
        
        return strSize.width
    }
}




@objc public class Target: NSObject {
    @objc weak var target : NSObject?
    @objc var selector : Selector?
    
    @objc func perform(object: Any!) {
        target?.perform(selector, with: object)
    }
    
    @objc func doAction(object: Any!) {
        target?.perform(selector, with: object)
    }
    
    @objc func perform(object1: Any!, object2: Any!) {
        target?.perform(selector, with: object1, with: object2)
    }
    
    @objc init(target:NSObject?, selector:Selector?) {
        super.init()
        self.selector = selector
        self.target = target
    }
    
}

Demo 地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市茬贵,隨后出現(xiàn)的幾起案子簿透,更是在濱河造成了極大的恐慌,老刑警劉巖解藻,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件老充,死亡現(xiàn)場離奇詭異,居然都是意外死亡螟左,警方通過查閱死者的電腦和手機(jī)啡浊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胶背,“玉大人巷嚣,你說我怎么就攤上這事∏鳎” “怎么了廷粒?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長红且。 經(jīng)常有香客問我坝茎,道長,這世上最難降的妖魔是什么暇番? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任嗤放,我火速辦了婚禮,結(jié)果婚禮上奔誓,老公的妹妹穿的比我還像新娘斤吐。我一直安慰自己搔涝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布和措。 她就那樣靜靜地躺著庄呈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪派阱。 梳的紋絲不亂的頭發(fā)上诬留,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機(jī)與錄音贫母,去河邊找鬼文兑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛腺劣,可吹牛的內(nèi)容都是我干的绿贞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼橘原,長吁一口氣:“原來是場噩夢啊……” “哼籍铁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起趾断,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拒名,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后芋酌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體增显,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年脐帝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了同云。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,977評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡腮恩,死狀恐怖梢杭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秸滴,我是刑警寧澤武契,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站荡含,受9級特大地震影響咒唆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜释液,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一全释、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧误债,春花似錦浸船、人聲如沸妄迁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽登淘。三九已至,卻和暖如春封字,著一層夾襖步出監(jiān)牢的瞬間黔州,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工阔籽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留流妻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓笆制,卻偏偏與公主長得像绅这,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子项贺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評論 2 355