摘要:通過可以將和連接起來,當(dāng)和連接后,獲得的預(yù)覽幀數(shù)據(jù)就可以通過顯示在屏幕上了。預(yù)覽幀數(shù)據(jù)傳遞給,實現(xiàn)預(yù)覽圖像的顯示。這里預(yù)覽幀數(shù)據(jù)對應(yīng)的預(yù)覽圖像暫且稱作相機(jī)預(yù)覽圖像。拍攝幀數(shù)據(jù)可以生成位圖文件,最終保存成或者等格式的圖片。
歡迎大家前往騰訊云+社區(qū),獲取更多騰訊海量技術(shù)實踐干貨哦~
本文由QQ空間開發(fā)團(tuán)隊發(fā)表于云+社區(qū)專欄
最近我負(fù)責(zé)開發(fā)了一個跟Android相機(jī)有關(guān)的需求,新功能允許用戶使用手機(jī)攝像頭,快速拍攝特定尺寸(1:1或3:4)的照片,并支持在拍攝出的照片上做貼紙相關(guān)的操作。由于之前沒有接觸過Android相機(jī)開發(fā),所以在整個開發(fā)過程中踩了不少坑,費了不少時間和精力。這篇文章總結(jié)了Android相機(jī)開發(fā)的相關(guān)知識、流程,以及容易遇到的坑,希望能幫助今后可能會接觸Android相機(jī)開發(fā)的朋友快速上手,節(jié)省時間,少走彎路。
一.Android中開發(fā)相機(jī)應(yīng)用的兩種方式Android系統(tǒng)提供了兩種使用手機(jī)相機(jī)資源實現(xiàn)拍攝功能的方法,一種是直接通過Intent調(diào)用系統(tǒng)相機(jī)組件,這種方法快速方便,適用于直接獲得照片的場景,如上傳相冊,微博、朋友圈發(fā)照片等。另一種是使用相機(jī)API來定制自定義相機(jī),這種方法適用于需要定制相機(jī)界面或者開發(fā)特殊相機(jī)功能的場景,如需要對照片做裁剪、濾鏡處理,添加貼紙,表情,地點標(biāo)簽等。這篇文章主要是從如何使用相機(jī)API來定制自定義相機(jī)這個方向展開的。
二.相機(jī)API中關(guān)鍵類解析通過相機(jī)API實現(xiàn)拍攝功能涉及以下幾個關(guān)鍵類和接口:
Camera:最主要的類,用于管理和操作camera資源。它提供了完整的相機(jī)底層接口,支持相機(jī)資源切換,設(shè)置預(yù)覽/拍攝尺寸,設(shè)定光圈、曝光、聚焦等相關(guān)參數(shù),獲取預(yù)覽/拍攝幀數(shù)據(jù)等功能,主要方法有以下這些:
open():獲取camera實例。
setPreviewDisplay(SurfaceHolder):綁定繪制預(yù)覽圖像的surface。surface是指向屏幕窗口原始圖像緩沖區(qū)(raw buffer)的一個句柄,通過它可以獲得這塊屏幕上對應(yīng)的canvas,進(jìn)而完成在屏幕上繪制View的工作。通過surfaceHolder可以將Camera和surface連接起來,當(dāng)camera和surface連接后,camera獲得的預(yù)覽幀數(shù)據(jù)就可以通過surface顯示在屏幕上了。
setPrameters設(shè)置相機(jī)參數(shù),包括前后攝像頭,閃光燈模式、聚焦模式、預(yù)覽和拍照尺寸等。
startPreview():開始預(yù)覽,將camera底層硬件傳來的預(yù)覽幀數(shù)據(jù)顯示在綁定的surface上。
stopPreview():停止預(yù)覽,關(guān)閉camra底層的幀數(shù)據(jù)傳遞以及surface上的繪制。
release():釋放Camera實例
takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg):這個是實現(xiàn)相機(jī)拍照的主要方法,包含了三個回調(diào)參數(shù)。shutter是快門按下時的回調(diào),raw是獲取拍照原始數(shù)據(jù)的回調(diào),jpeg是獲取經(jīng)過壓縮成jpg格式的圖像數(shù)據(jù)的回調(diào)。
SurfaceView:用于繪制相機(jī)預(yù)覽圖像的類,提供給用戶實時的預(yù)覽圖像。普通的view以及派生類都是共享同一個surface的,所有的繪制都必須在UI線程中進(jìn)行。而surfaceview是一種比較特殊的view,它并不與其他普通view共享surface,而是在內(nèi)部持有了一個獨立的surface,surfaceview負(fù)責(zé)管理這個surface的格式、尺寸以及顯示位置。由于UI線程還要同時處理其他交互邏輯,因此對view的更新速度和幀率無法保證,而surfaceview由于持有一個獨立的surface,因而可以在獨立的線程中進(jìn)行繪制,因此可以提供更高的幀率。自定義相機(jī)的預(yù)覽圖像由于對更新速度和幀率要求比較高,所以比較適合用surfaceview來顯示。
SurfaceHolder:surfaceholder是控制surface的一個抽象接口,它能夠控制surface的尺寸和格式,修改surface的像素,監(jiān)視surface的變化等等,surfaceholder的典型應(yīng)用就是用于surfaceview中。surfaceview通過getHolder()方法獲得surfaceholder 實例,通過后者管理監(jiān)聽surface 的狀態(tài)。
SurfaceHolder.Callback接口:負(fù)責(zé)監(jiān)聽surface狀態(tài)變化的接口,有三個方法:
surfaceCreated(SurfaceHolder holder):在surface創(chuàng)建后立即被調(diào)用。在開發(fā)自定義相機(jī)時,可以通過重載這個函數(shù)調(diào)用camera.open()、camera.setPreviewDisplay(),來實現(xiàn)獲取相機(jī)資源、連接camera和surface等操作。
surfaceChanged(SurfaceHolder holder, int format, int width, int height):在surface發(fā)生format或size變化時調(diào)用。在開發(fā)自定義相機(jī)時,可以通過重載這個函數(shù)調(diào)用camera.startPreview來開啟相機(jī)預(yù)覽,使得camera預(yù)覽幀數(shù)據(jù)可以傳遞給surface,從而實時顯示相機(jī)預(yù)覽圖像。
surfaceDestroyed(SurfaceHolder holder):在surface銷毀之前被調(diào)用。在開發(fā)自定義相機(jī)時,可以通過重載這個函數(shù)調(diào)用camera.stopPreview(),camera.release()來實現(xiàn)停止相機(jī)預(yù)覽及釋放相機(jī)資源等操作。
三.自定義相機(jī)的開發(fā)過程定制一個自定義相機(jī)應(yīng)用,通常需要完成以下步驟,其流程圖如圖1所示:
檢測并訪問相機(jī)資源 檢查手機(jī)是否存在相機(jī)資源,如果存在,請求訪問相機(jī)資源。
創(chuàng)建預(yù)覽類 創(chuàng)建繼承自SurfaceView并實現(xiàn)SurfaceHolder接口的拍攝預(yù)覽類。此類能夠顯示相機(jī)的實時預(yù)覽圖像。
建立預(yù)覽布局 有了拍攝預(yù)覽類,即可創(chuàng)建一個布局文件,將預(yù)覽畫面與設(shè)計好的用戶界面控件融合在一起。
設(shè)置拍照監(jiān)聽器 給用戶界面控件綁定監(jiān)聽器,使其能響應(yīng)用戶操作(如按下按鈕), 開始拍照過程。
拍照并保存文件 將拍攝獲得的圖像轉(zhuǎn)換成位圖文件,最終輸出保存成各種常用格式的圖片。
釋放相機(jī)資源 相機(jī)是一個共享資源,必須對其生命周期進(jìn)行細(xì)心的管理。當(dāng)相機(jī)使用完畢后,應(yīng)用程序必須正確地將其釋放,以免其它程序訪問使用時,發(fā)生沖突。
圖1 定制自定義相機(jī)的過程
對應(yīng)到代碼編寫上可以分成三個步驟:
第一步:在AndroidManifest.xml中添加Camera相關(guān)功能使用的權(quán)限,具體聲明有以下這些:
第二步:編寫相機(jī)操作功能類CameraOperationHelper。采用單例模式來統(tǒng)一管理相機(jī)資源,封裝相機(jī)API的直接調(diào)用,并提供用于跟自定義相機(jī)Activity做UI交互的回調(diào)接口,其功能函數(shù)如下,主要有創(chuàng)建釋放相機(jī),連接開始關(guān)閉預(yù)覽界面,拍照,自動對焦,切換前后攝像頭,切換閃光燈模式等,具體實現(xiàn)可以參考官方API文檔。
第三步:編寫自定義相機(jī)Activity,主要是定制相機(jī)界面,實現(xiàn)UI交互邏輯,如按鈕點擊事件處理,icon資源切換,鏡頭尺寸切換動畫等。這里需要聲明一個SurfaceView對象來實時顯示相機(jī)預(yù)覽畫面。通過SurfaceHolder及其Callback接口來一同管理屏幕surface和相機(jī)資源的連接,相機(jī)預(yù)覽圖像的顯示/關(guān)閉。
四. 開發(fā)過程遇到的一些坑下面再講講我在開發(fā)自定義相機(jī)時踩過的一些坑:
1. Activity設(shè)為豎屏?xí)r,SurfaceView預(yù)覽圖像顛倒90度。說明這個問題之前,先介紹下Android手機(jī)上幾個方向的概念:
屏幕方向:在Android系統(tǒng)中,屏幕的左上角是坐標(biāo)系統(tǒng)的原點(0,0)坐標(biāo)。原點向右延伸是X軸正方向,原點向下延伸是Y軸正方向。
相機(jī)傳感器方向:手機(jī)相機(jī)的圖像數(shù)據(jù)都是來自于攝像頭硬件的圖像傳感器,這個傳感器在被固定到手機(jī)上后有一個默認(rèn)的取景方向,如下圖2所示,坐標(biāo)原點位于手機(jī)橫放時的左上角,即與橫屏應(yīng)用的屏幕X方向一致。換句話說,與豎屏應(yīng)用的屏幕X方向呈90度角。
圖2 相機(jī)傳感器方向示意圖
相機(jī)的預(yù)覽方向:由于手機(jī)屏幕可以360度旋轉(zhuǎn),為了保證用戶無論怎么旋轉(zhuǎn)手機(jī)都能看到“正確”的預(yù)覽畫面(這個“正確”是指顯示在UI預(yù)覽界面的畫面與人眼看到的眼前的畫面是一致的),Android系統(tǒng)底層根據(jù)當(dāng)前手機(jī)屏幕的方向?qū)D像傳感器采集到的數(shù)據(jù)進(jìn)行了旋轉(zhuǎn)處理,然后才送給顯示系統(tǒng),因此可以保證預(yù)覽畫面始終“正確”。在相機(jī)API中可以通過setDisplayOrientation()設(shè)置相機(jī)預(yù)覽方向。在默認(rèn)情況下,這個值為0,與圖像傳感器一致。因此對于橫屏應(yīng)用來說,由于屏幕方向和預(yù)覽方向一致,預(yù)覽圖像不會顛倒90度。但是對于豎屏應(yīng)用,屏幕方向和預(yù)覽方向垂直,所以會出現(xiàn)顛倒90度現(xiàn)象。為了得到正確的預(yù)覽畫面,必須通過API將相機(jī)的預(yù)覽方向旋轉(zhuǎn)90,保持與屏幕方向一致,如圖3所示。
圖3 相機(jī)預(yù)覽方向示意圖
(紅色箭頭為預(yù)覽方向,藍(lán)色方向為屏幕方向)
相機(jī)的拍照方向:當(dāng)點擊拍照按鈕,拍攝的照片是由圖像傳感器采集到的數(shù)據(jù)直接存儲到SDCard上產(chǎn)生的,因此,相機(jī)的拍照方向與傳感器方向是一致的。
2. SurfaceView預(yù)覽圖像、拍攝照片拉伸變形說明這個問題之前,同樣先說一下幾個跟相機(jī)有關(guān)的尺寸。
SurfaceView尺寸:即自定義相機(jī)應(yīng)用中用于顯示相機(jī)預(yù)覽圖像的View的尺寸,當(dāng)它鋪滿全屏?xí)r就是屏幕的大小。這里surfaceview顯示的預(yù)覽圖像暫且稱作手機(jī)預(yù)覽圖像。
Previewsize:相機(jī)硬件提供的預(yù)覽幀數(shù)據(jù)尺寸。預(yù)覽幀數(shù)據(jù)傳遞給SurfaceView,實現(xiàn)預(yù)覽圖像的顯示。這里預(yù)覽幀數(shù)據(jù)對應(yīng)的預(yù)覽圖像暫且稱作相機(jī)預(yù)覽圖像。
Picturesize:相機(jī)硬件提供的拍攝幀數(shù)據(jù)尺寸。拍攝幀數(shù)據(jù)可以生成位圖文件,最終保存成.jpg或者.png等格式的圖片。這里拍攝幀數(shù)據(jù)對應(yīng)的圖像稱作相機(jī)拍攝圖像。圖4說明了以上幾種圖像及照片之間的關(guān)系。手機(jī)預(yù)覽圖像是直接提供給用戶看的圖像,它由相機(jī)預(yù)覽圖像生成,拍攝照片的數(shù)據(jù)則來自于相機(jī)拍攝圖像。
圖4 幾種圖像之間的關(guān)系
下面說下我在開發(fā)過程中遇到的三種拉伸變形現(xiàn)象:
1、手機(jī)預(yù)覽畫面中物體被拉伸變形。
2、拍攝照片中物體被拉伸變形。
3、點擊拍照瞬間,手機(jī)預(yù)覽畫面會停頓下,此時的圖像是拉伸變形的,然后預(yù)覽畫面恢復(fù)后圖像又正常了。
現(xiàn)象1的原因是SurfaceView和Previewsize的長寬比率不一致。因為手機(jī)預(yù)覽視圖的圖像是由相機(jī)預(yù)覽圖像根據(jù)SurfaceView大小縮放得來的,當(dāng)長寬比不一致時必然會導(dǎo)致圖像變形。后兩個現(xiàn)象的原因則是Previewsize和Picturesize的長寬比率不一致所致,查了相關(guān)的資料,發(fā)現(xiàn)其具體原因跟某些手機(jī)相機(jī)硬件的底層實現(xiàn)有關(guān)。總之為了避免以上幾種變形現(xiàn)象的發(fā)生,在開發(fā)時最好將SurfaceView、PreviewSize、PictureSize三個尺寸保證長寬比例一致。具體實現(xiàn)可以先通過camera.getSupportedPreviewSizes()和camera.getSupportedPictureSizes()獲得相機(jī)硬件支持的所有預(yù)覽和拍攝尺寸,然后在里面篩選出和SurfaceView的長寬比一致并且大小合適的尺寸,通過camera.setPrameters來更新設(shè)置。注意:市場上手機(jī)相機(jī)硬件支持的尺寸一般都是主流的4:3或者16:9,所以SurfaceView尺寸不能太奇葩,最好也設(shè)置成這樣的長寬比。
3. 各種crash前兩個Crash的原因是:相機(jī)硬件在聚焦和拍照前必須要保證已經(jīng)連接到surface,并且開啟相機(jī)預(yù)覽,surface有收到預(yù)覽數(shù)據(jù)。如果在還沒有執(zhí)行camera. setPreviewDisplay或者未調(diào)用camera. startPreview之前,就調(diào)用camera.autofocus或camera.takepicture,就會出現(xiàn)這個運行時異常。對應(yīng)到自定義相機(jī)的代碼中,要注意在拍照按鈕事件響應(yīng)中執(zhí)行camera.autofocus或camera.takepicture前,一定要檢驗camera有沒有設(shè)置預(yù)覽Surfaceview并開啟了相機(jī)預(yù)覽。這里有個方法可以判斷預(yù)覽狀態(tài):Camera.setPreviewCallback是預(yù)覽幀數(shù)據(jù)的回調(diào)函數(shù),它會在SurfaceView收到相機(jī)的預(yù)覽幀數(shù)據(jù)時被調(diào)用,因此在里面可以設(shè)置是否允許對焦和拍照的標(biāo)志位。
還有一點要注意,camera.takePicture()在執(zhí)行過程中會執(zhí)行camera.stopPreview來獲取拍攝幀數(shù)據(jù),表現(xiàn)為預(yù)覽畫面卡住,而如果此時用戶點擊了按鈕的話,也就是調(diào)用camera.takepicture,也會出現(xiàn)上面的crash,因此在開發(fā)時,可能還需要屏蔽拍照按鈕的連續(xù)點擊。
第三個crash則涉及圖像的裁剪,由于要支持1:1或者4:3尺寸鏡頭,所以會需要對預(yù)覽視圖進(jìn)行裁剪,由于是豎屏應(yīng)用,所以裁剪區(qū)域的坐標(biāo)系跟相機(jī)傳感器方向是成90度角的,表現(xiàn)在裁剪里就是,屏幕上的x方向,對應(yīng)在拍攝圖像上是高度方向,而屏幕上的y方向,對應(yīng)到拍攝圖像上則是寬度方向。因此在計算時要一定注意坐標(biāo)系的轉(zhuǎn)換以及越界保護(hù)。
4. 前置攝像頭的鏡像效果Android相機(jī)硬件有個特殊設(shè)定,就是對于前置攝像頭,在展示預(yù)覽視圖時采用類似鏡面的效果,顯示的是攝像頭成像的鏡像。而拍攝出的照片則仍采用攝像頭成像。看到這里,大家可能會有些懷疑,不妨現(xiàn)在就試試自己Android手機(jī)上的前置攝像頭,對比下預(yù)覽圖像和拍攝出照片的區(qū)別。這是由于底層相機(jī)在傳遞前置攝像頭預(yù)覽數(shù)據(jù)時做了水平翻轉(zhuǎn)變換,即將x方向鏡像翻轉(zhuǎn)180度。這個變化對之前豎屏預(yù)覽的方向也會造成影響,本來對于后置攝像頭旋轉(zhuǎn)90度即可使預(yù)覽視圖正確,而對前置攝像頭,如果也旋轉(zhuǎn)90度的話,看到的預(yù)覽圖像則是上下顛倒的(因為x方向翻轉(zhuǎn)了180度),因此必須再旋轉(zhuǎn)180度,才能顯示正確,如圖5所示,大家可以結(jié)合之前相機(jī)預(yù)覽方向的示意圖一起理解。
圖5 前置攝像頭的預(yù)覽方向示意圖
此外,由于拍攝圖像并沒有做水平翻轉(zhuǎn),所以對于前置攝像頭拍出來的照片,用戶會發(fā)現(xiàn)跟預(yù)覽時所見的是左右翻轉(zhuǎn)的。這個在一定程度上會影響用戶體驗。為了解決這個問題,可以對前置攝像頭拍攝的圖像在生成位圖文件時增加一個水平翻轉(zhuǎn)矩陣變換。
5. 鎖屏下相機(jī)資源的釋放問題為了節(jié)省手機(jī)電量,不浪費相機(jī)資源,在開發(fā)的自定義相機(jī)里,如果預(yù)覽圖像已不需要顯示,如按Home鍵盤切換后臺或者鎖屏后,此時就應(yīng)該關(guān)閉預(yù)覽并把相機(jī)資源釋放掉。參考官方API文檔,當(dāng)surfaceView變成可見時,會創(chuàng)建surface并觸發(fā)surfaceHolder.callback接口中surfaceCreated回調(diào)函數(shù)。而surfaceview變成不可見時,則會銷毀surface,并觸發(fā)surfacedestroyed回調(diào)函數(shù)。我們可以在對應(yīng)的回調(diào)函數(shù)里,處理相機(jī)的相關(guān)操作,如連接surface、開啟/關(guān)閉預(yù)覽。 至于相機(jī)資源釋放,則可以放在Acticity的onpause里執(zhí)行。相應(yīng)的,要重新恢復(fù)預(yù)覽圖像時,可以把相機(jī)資源申請和初始化放在Acticity的onResume里執(zhí)行,然后通過創(chuàng)建surfaceview,將camera和surface相連并開啟預(yù)覽。
但是在開發(fā)過程中發(fā)現(xiàn),對于按HOME鍵切后臺場景,程序可以正常運行。對于鎖屏場景,則在重新申請相機(jī)資源時會發(fā)生crash,說相機(jī)資源訪問失敗。那么原因是什么呢?我在代碼里增加了調(diào)試log, 檢查了代碼的執(zhí)行順序,結(jié)果如下:
在自定義相機(jī)頁面按HOME鍵時的執(zhí)行流程:
程序運行->按HOME鍵
Activity調(diào)用的順序是onPause->onStop
SurfaceView調(diào)用了surfaceDestroyed方法
然后再切回程序
Activity調(diào)用的順序是onRestart->onStart->onResume
SurfaceView調(diào)用了surfaceCreated->surfaceChanged方法
而對于鎖屏,其執(zhí)行流程則是:
Activity只調(diào)用onPause方法
解鎖后Activity調(diào)用onResume方法
SurfaceView中surfaceholder.callback的所有方法都沒有執(zhí)行
問題找到了,由于鎖屏?xí)r,callback的回調(diào)方法沒有執(zhí)行,導(dǎo)致相機(jī)和預(yù)覽的連接還沒有斷開,相機(jī)資源就被釋放了,所以導(dǎo)致在重新申請相機(jī)資源時,系統(tǒng)報crash。根據(jù)上面的文檔,推測是鎖屏下系統(tǒng)并沒有改變surfaceview的可見性,于是我嘗試在onPause和onResume時通過手動設(shè)置surfaceview的visibile屬性,結(jié)果發(fā)現(xiàn)可以正常觸發(fā)回調(diào)函數(shù)了。由于在切后臺或者鎖屏?xí)r,用戶本來就應(yīng)該看不到surfaceview,因此這種手動更改surfaceview的可見性的方法,并不會對用戶的體驗造成影響。
問答
Android - 如何修復(fù)權(quán)限異常?
相關(guān)閱讀
深入理解Autorelease Pool
ComponentKit框架解析之一—初識CK
Android 內(nèi)存泄漏分析心得
【每日課程推薦】機(jī)器學(xué)習(xí)實戰(zhàn)!快速入門在線廣告業(yè)務(wù)及CTR相應(yīng)知識
此文已由作者授權(quán)騰訊云+社區(qū)發(fā)布,更多原文請點擊
搜索關(guān)注公眾號「云加社區(qū)」,第一時間獲取技術(shù)干貨,關(guān)注后回復(fù)1024 送你一份技術(shù)課程大禮包!
海量技術(shù)實踐經(jīng)驗,盡在云加社區(qū)!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/77203.html
摘要:前言阿里巴巴機(jī)器智能實驗室線下智能團(tuán)隊從年底開始涉及線下智能領(lǐng)域,從算法工程產(chǎn)品化業(yè)務(wù)落地多個方面入手,與合作伙伴們一起取得了一些小小的成績。目前,該套工具作為推薦的量化工具廣泛應(yīng)用在阿里集團(tuán)內(nèi)多個線下業(yè)務(wù)場景中。 showImg(https://segmentfault.com/img/remote/1460000019246850); 阿里妹導(dǎo)讀:AI 技術(shù)已經(jīng)從互聯(lián)網(wǎng)走向零售、...
摘要:在移動端頁面使用上傳文件或者圖片時,和安卓的展現(xiàn)方式有很多不一樣。 在移動端頁面使用上傳文件或者圖片時,IOS和安卓的展現(xiàn)方式有很多不一樣。 input 有 captrure屬性,取值:camera:相機(jī);camcorder:攝像;microphone:錄音 在安卓想要調(diào)用相機(jī)需要添加capture屬性,于是我在IOS和Android上進(jìn)行了三端測試! 結(jié)果如下: 1. 安卓: 【...
摘要:在移動端頁面使用上傳文件或者圖片時,和安卓的展現(xiàn)方式有很多不一樣。 在移動端頁面使用上傳文件或者圖片時,IOS和安卓的展現(xiàn)方式有很多不一樣。 input 有 captrure屬性,取值:camera:相機(jī);camcorder:攝像;microphone:錄音 在安卓想要調(diào)用相機(jī)需要添加capture屬性,于是我在IOS和Android上進(jìn)行了三端測試! 結(jié)果如下: 1. 安卓: 【...
閱讀 4116·2023-04-26 02:07
閱讀 3742·2021-10-27 14:14
閱讀 2965·2021-10-14 09:49
閱讀 1686·2019-08-30 15:43
閱讀 2696·2019-08-29 18:33
閱讀 2432·2019-08-29 17:01
閱讀 976·2019-08-29 15:11
閱讀 678·2019-08-29 11:06