show code block

2023年11月29日 星期三

協程(coroutine) - 協程為什麼要學它?

 Coroutine 協程

再強調一次

協程就是由kotlin官方所提供的線程api

  
        //Thread
        Thread {

        }.start()

        //Executor
        val executor = Executors.newCachedThreadPool()
        executor.execute {

        }

        //coroutine
        launch {

        }
  
  
Rxjava都有了 coroutine的優勢在哪?
本質上跟其他的線程api一樣,方便
他借助了kotlin的語言優勢,所以他基於那些Java之上的方案會更方便一點
最重要的是
他可以用看起來同步的方式,寫出異步代碼

  
   
        val user = api.getUser()//後台線程
        tvName.text = user.name //主線程
  
  

這就是kotlin最有名的『非阻塞式掛起』suspend
Coroutine最大的好處,是你可以把不同線程的代碼寫在同一塊代碼塊裡面
coroutine:
  
   
        launch(Dispatchers.Main) { // (主線程)
        
            val result = api.fetchData() // 異步操作,比如獲取網絡數據(後台線程)
            tvName.text = result.name // 在這裡處理結果,如更新UI(主線程)
            //上下兩行代碼,可以先切走在切回來,這是java完全做不到的事情
            
        }

  
正常callback寫法:
  
   
        api.getUser()
        .enqueue(object:Callback<User>{
        ...
        override fun onResponse(call:Call<User>,
                                response:Response<User>){
        
          runOnUiThread{
          
          tvName.text = response.body().name
          
          }
                               
         }
        
        })

  
Callback的寫法我們早就爛到骨頭裡了,你為什麼還是要學習Coroutine?
省了超多callback代碼,超簡潔 
消除了callback 多線程的操作難度直接抹平了 
不是10 -> 9 
而是 1 -> 0

我舉一個差異最大的例子
像是我要做一個
兩次網路請求
並把這兩個輸出結果拿去做第三個網路請求

callback式的開發,要做這種工作非常困難
於是我們可能會選擇妥協
變成完成a後再call b的先後請求,這就是標準的垃圾代碼了
明明可以並行的兩個請求,由於我自身能力不足
我讓網路時間等待長了一倍,也就是性能上差了一倍

如果是使用協程
  
launch(Dispatchers.Main) {

    val userDetails = async {

        api.getUserDetails(user) // 獲取用戶詳細資料

    }

    val userPosts = async {

        api.getUserPosts(user) // 獲取用戶發布的帖子

    }

    val combinedData = suspendingMerge(userDetails, userPosts) // 合併資料
    // 接下來可以處理合併後的資料

}
 
coroutine由於消除了併發之間協作的難度
可以輕鬆地寫出複雜的併發代碼
甚至有些不可能實現的併發任務,變成可能甚至變得很簡單
這些才是協程的優勢所在

但這時你可能會有個疑惑
你要在後台執行任務?切協程
要在主線程執行任務?切協程
  
 
          launch(Dispatchers.IO) {
          
              val fileData = readFile(fileId) // 在IO線程中讀取文件數據

              launch(Dispatchers.Main) {
                  fileTextView.setText(fileData) // 在主線程中更新文本視圖
              }
              
          }

   
 這不也是callback地獄?
 讓code很髒很不簡潔?
這邊就要講到
coroutine有一個很厲害的函數 withContext
  
          launch(Dispatchers.Main) {
              val fileContent = withContext(Dispatchers.IO) {
                  readFile(fileId) // 在IO線程中讀取文件內容
              }

              fileTextView.setText(fileContent) // 在主線程中設置文本視圖的內容
          }
 
可以寫成這樣,看起來區別不大
但如果你有更多的線程切換,優勢就體現出來了

由於有自動切回的功能,Coroutine消除了併發代碼在協作時的嵌套
 
        launch(Dispatchers.IO){
            ...
            launch(Dispatchers.Main){
                ...
                launch(Dispatchers.IO){
                    ...
                    launch(Dispatchers.Main){
                    }
                }
            }
        }
   
由於有了『自動切回來的功能』
寫成消除了在併發代碼協作時的嵌套
直接寫成上下關係代碼
就能讓Coroutine之間進行協作
 
        launch(Dispatchers.Main){
            withContext(Dispatchers.IO){
                ...
            }
                ...
            withContext(Dispatchers.IO){
                ...
            }
                ...
        }
     
這就是協程 
『協作式的例程』

而且由於消除了嵌套
你可以把withContext放到函數裡面
用她包著函數的實際業務代碼
但function要在前方用掛起函數
 
        launch(Dispatchers.Main) {
        
            val fileContent = suspendingReadFile(fileId)
            fileTextView.setText(fileContent)
            
        }

        suspend fun suspendingReadFile(fileId: String): String {
        
            return withContext(Dispatchers.IO) {
                readFile(fileId) // 在IO線程中讀取文件
            }
            
        }

     
suspend是coroutine最核心的關鍵詞
他也是最難懂,最多被誤解的點
『suspend是掛起,非阻塞式的,他並不會阻擋你的線程』
這你看得懂嗎? 我一開始是看不懂

再說一次
協程是什麼?
一個線程框架

好在哪?好在方便
最方便的地方在哪?
他可以在同一個代碼塊中做線程切換














2023年11月1日 星期三

協程(coroutine) - suspend、Dispatchers、Scope.launch (初略介紹)

Kotlin 協程是一個強大的同時也相對輕量級的工具,可以使異步編程變得更加容易和直觀。


基本概念:


協程: 是輕量級的執行緒,它們在某些情境下可以被暫停並在稍後恢復。
挂起函数 (Suspend Functions): 可以暫停當前的協程而不阻塞執行緒。它們使用 suspend 關鍵字。
使用 launch 和 async 建立協程:

launch 是最常見的啟動協程的方式,它返回一個 Job。
async 用於啟動一個協程並返回一個 Deferred<T>,通常用於異步計算結果。
協程上下文和調度器 (Dispatchers):

協程

總是在某個上下文中運行,最常見的調度器有 Dispatchers.Main, Dispatchers.IO, 和 Dispatchers.Default。

withContext: 在不同的調度器或上下文中切換協程的工作。

組合協程: 使用 join 和 await 來等待協程完成。

錯誤處理: 學習如何使用 try/catch 捕獲協程中的異常以及如何設置協程的異常處理程序。

Flow: Kotlin 提供的一種冷數據流工具,專為協程設計。與 RxJava 相似,但更為簡潔。

協程測試: 使用 runBlockingTest 和其他工具來測試協程。

實際應用: 在實際項目中使用協程,如 Android 的 viewModelScope。

進階主題:

協程共享狀態和同步。
通道 (Channels) 和 Actor 模型。
學習資源:

建議開始時可以從 Kotlin 官方文檔開始,然後進入到 Android 的協程使用文檔。隨著練習和深入學習,你會更加熟悉協程的概念和用法。


先來了解要如何使用它

Scope.launch


  CoroutineScope(Dispatchers.Main).launch {
    // 在主線程上執行的協程代碼
    val data = withContext(Dispatchers.IO) {
        // 切換到 I/O 線程,執行阻塞性 I/O 任務
        loadDataFromDisk()
    }
    // 回到主線程,更新 UI
    updateUI(data)
}
  

當你使用 Scope.launch { ... } 這種結構時,你正在在指定的協程範圍(Scope)內啟動一個新的協程。讓我們分步驟來詳細解釋這個過程:

1. 協程範圍(CoroutineScope)

每個協程都運行在某個 CoroutineScope 內,這個範圍決定了協程的生命周期。協程會隨著它的範圍被取消而取消。
範圍可以代表應用的結構單位,比如一個 Android 的 Activity 或 ViewModel。例如,viewModelScope 是綁定到 ViewModel 生命周期的一個範圍,在 ViewModel 被清理時,所有在這個範圍中的協程都會自動取消。

2. launch 函數

launch 是一個擴展函數,用於在協程範圍內啟動新協程。它立即返回一個 Job 對象,這個對象代表了協程。
launch 不會阻塞當前線程,並且它內部的協程代碼會在協程調度器決定的線程上異步執行。

3. 協程的生命周期

協程的生命周期與它的範圍(CoroutineScope)緊密相連。當範圍被取消時,所有在該範圍內的協程都會被取消。
如果協程內發生未捕獲的異常,默認情況下它會導致整個範圍的取消,這樣範圍內的其他協程也會被取消。

4. 調度器(Dispatcher)

launch 可以接受一個可選的 CoroutineContext 參數,通常是一個 Dispatcher。這個 Dispatcher 決定了協程應該在哪個線程或線程池上執行。
如果沒有指定調度器,協程將繼承它的範圍的調度器。例如,在 viewModelScope.launch { ... } 中,如果沒有指定調度器,那麼 viewModelScope 的默認調度器將被使用。




*Dispatchers:

在 Kotlin 協程中,Dispatchers 是用來指定協程應該在哪種類型的線程上執行的。Dispatchers 可以決定協程的行為和性能特性,如何選擇合適的 Dispatcher 對於寫出高效和響應式的應用程序非常重要。

Kotlin 協程中提供了幾個核心的 Dispatcher:

1. Dispatchers.Main

用於 Android 的主線程,用於更新 UI 和處理與 UI 相關的任務。
任何在這個調度器上啟動的協程都會在 Android 的主線程上執行。
不應該用於執行耗時或阻塞性的操作,因為這會導致 UI 卡頓。

2. Dispatchers.IO

專為 I/O 任務(如網絡請求、讀取和寫入文件)而設計。
擁有一個可擴展的線程池,可以根據需要增加線程數量。
適合用於執行可能會阻塞線程的 I/O 任務。

3. Dispatchers.Default

專為 CPU 密集型工作,如大規模的數據處理和計算任務。
使用一個固定大小的線程池,大小預設與處理器的核心數量相同。
不適合 I/O 任務,因為那會浪費有限的計算資源。

4. Dispatchers.Unconfined

這個調度器將協程執行在當前的線程上,但只在第一次暫停點之前。暫停後,它會在恢復協程的那個線程上繼續執行。
它不提供任何確定性的線程模型。通常不推薦使用,除非你非常清楚為什麼需要它,因為它可能會導致一些難以發現的錯誤。

5. 自定義 Dispatcher

可以通過使用 Executor 來創建自定義的 CoroutineDispatcher。
這可以讓你根據應用的特定需求來定制線程的行為。

通常比較會使用到的就只有前三個
// 使用 Main 調度器,在主線程上執行
GlobalScope.launch(Dispatchers.Main) {
    // 更新 UI
}

// 使用 IO 調度器,進行 I/O 任務
GlobalScope.launch(Dispatchers.IO) {
    // 執行 I/O 操作
}

// 使用 Default 調度器,進行計算任務
GlobalScope.launch(Dispatchers.Default) {
    // 執行耗時的計算
}

// 使用 Unconfined 調度器
GlobalScope.launch(Dispatchers.Unconfined) {
    // 慎用,協程可以在任何線程上恢復
}

// 創建自定義的 Dispatcher
val myDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
GlobalScope.launch(myDispatcher) {
    // 在自定義的線程上執行
}





*挂起函數(Suspending Functions) -  suspend

他是coroutine協程的核心概念之一

使用場景:
掛起函數通常用於執行耗時的任務
如:
網絡通信:發送請求並等待響應。
數據庫操作:執行查詢並等待結果。
文件處理:讀寫大文件操作。
任何可能耗時的操作,你不想在主線程上執行。

工作原理:
當呼叫一個挂起函數時,如果該函數需要等待(例如,等待網絡響應),它會挂起包含它的協程,而不是阻塞線程。等到挂起的操作完成時,協程的執行將恢復。

特點:
只能被其他掛起函數或協程的建構器內呼叫。
可以調用其他掛起函數。
不會阻塞當前線程,從而允許其他任務在同一個線程上運行。

使用方式:
他的使用方式很簡單
只需要在fun前面加入suspend就可以宣告成掛起函數
suspend fun fetchDoc(id: String): Document {
    // 模擬耗時操作
    delay(1000)
    return Document(id)
}

這樣就變成一個掛起函數了
但並不是只要加了suspend他就會變成不阻塞主線程的function
必須配合

  //..在activity內使用
  lifecycleScope.launch {
  
            // 在這裡執行你的異步操作
         val doc = fetchDoc("123") // 調用掛起函數
         println(doc)
         
        }

而你想調用suspend function必須也是suspend function
這樣就就是一個簡單的掛起函數使用方式

來講一個我們最常用的使用情境
call api的情況
   
   // Repository中
suspend fun fetchUserData(): UserData {

return withContext(Dispatchers.IO) { // 切換到IO調度器
// 執行網絡請求,並等待結果
apiService.getUserData()

}
}

  // ViewModel中
fun fetchUserData() {

viewModelScope.launch { // 在ViewModel作用域內啟動協程

val userData = repository.fetchUserData() // 調用掛起函數獲取數據
userLiveData.postValue(userData) // 更新LiveData

}

}


  
上面範例中使用到了withContext(Dispatchers.IO),你告訴協程應該在一個專門用於 I/O 任務的線程池中運行,意思也就是非主線程。
什麼是IO?
I/O 是 Input/Output 的縮寫,即“輸入/輸出”。
後面會再詳述

Scope.launch的用法意思就是在你想要創建的線程上,創建一個協程Coroutine
就好比你在主線程上創建了協程,代表什麼呢?

想像主線程像是一間餐廳的主廚,他需要處理各種事情:烹飪、盤點庫存、接電話訂單等。
這間餐廳就是你的應用,而顧客希望服務快速且不出錯。

當你使用scope.launch時,就像主廚叫了一個助手來處理一項特定的任務,比如煮一鍋湯。
這個助手在同一個廚房中(主線程上)工作,但是他有自己的工作空間,不會干擾主廚做其他事情。這就是協程,它運行在主線程中但是不阻塞主廚的其他工作。

suspend函數就像是這個助手在煮湯時需要等待湯煮開。助手不會站在爐子旁邊發呆等湯煮開,他會做其他事情,比如切菜,直到湯煮開了他再回來繼續這項任務。

這就是掛起的概念:暫時將某個需要等待的任務放到一邊,直到可以繼續進行為止。

所以,當你在使用協程時,你基本上是在告訴主廚:「這裡有一些任務可以同時進行,但你不需要時刻盯著它們,只需要確保在必要的時候回來處理即可。」這樣主廚(主線程)的時間就被利用得更高效了,顧客(用戶)也會因為服務迅速而滿意。













2022年5月10日 星期二

DEBUG - CardView cardCornerRadius 圓角、陰影沒效果?


點擊AndroidManifest.xml

並找到你要顯示圓角的Activity,加入


 android:hardwareAccelerated="true"


如下方

 <activity
            android:name=".views.member.MemberAuthorisationActivity"
            ...
            android:hardwareAccelerated="true"
            ... /> 

即可正常運作~


或是可以直接指定給application


 <application android:hardwareAccelerated="true" ...>


2019年1月15日 星期二

Android台北景點 專案(一) - retrofit

前言/雜感:

記得以前談過
徐曉冬打爆中國武術太極和永春高手的雜感

最近新聞又出來了,很有感觸啊。
看那徐曉冬根本戲謔,再看看中國武術大師的肚子

各種強烈的對比

中國武術在完全無法充分交流的情況下,真的變成練身體健康的舞蹈了

不甚唏噓

回歸正題

用一個專案講 retrofit 和 observer 觀察者模式

我們拿台北市的公開json資料來串接json

可先行參考台北市政府的openData

今日實作




重點程式碼:




import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;

public interface CallAttractionData {
    @GET("opendata/datalist/apiAccess?scope=resourceAquire&rid=36847f3f-deff-4183-a5bb-800737591de5")
    Call<AttractionsModel> getCall();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://data.taipei/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    CallAttractionData apiService = retrofit.create(CallAttractionData.class);

}

Call<Model> getCall();
Model為你要解析的json格式



實作:


目前會動到的部分


 1、添加權限
 2、import retrofit第三方
 3、使用retrofit
 4、讓json資料呈現在畫面上

添加權限

在manifest 加入網路權限
<uses-permission android:name="android.permission.INTERNET" />


import retrofit第三方 先在APP層的build.gradle加入retrofit


    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
    implementation 'com.squareup.okhttp3:okhttp:3.12.0'

在看圖說故事
添加GsonFormat這邊是使用gsonformat外掛讓你不用再自己寫model。
 再轉到台北市政府openData的資料 打開下方的網址,會看到長這樣
jon資料
此時我們複製results內的item,只要複製前兩個
可以讓剛剛你在android studio內安裝的gsonFormat判斷是一個陣列就行了

使用retrofit 

創建一個AttractionsModel來承接你的json資料
在你的AttractionsModel內按下熱鍵 Alt+insert (mac:command + N)
會跳出懶人框

懶人框

如果剛剛的gsonFormat有安裝成功,會出現上圖的GsonFormat
進入後複製剛剛台北市政府的json資料丟入,
就會幫妳生成完整的資料。
你會發現資料很多都是你用不到的垃圾,或是你看不懂的欄位。
這時候你可以自己過濾篩選,或是改成你想要的欄位名稱
GsonFormat工具
這邊比較需要注意的是,json提供的資料龐大,可以在results內的item數量很多,可以只取兩個就好。
如下方
{
result: {
limit: 1000,
offset: 0,
count: 319,
sort: "",
results: [
{
info: "新北投站下車,沿中山路直走即可到達公車:216、218、223、230、266、602、小6、小7、小9、、小22、小25、小26至新北投站下車",
stitle: "新北投溫泉區",
xpostDate: "2016/07/07",
longitude: "121.508447",
REF_WP: "10",
avBegin: "2010/02/14",
langinfo: "10",
MRT: "新北投",
SERIAL_NO: "2011051800000061",
RowNumber: "1",
CAT1: "景點",
CAT2: "養生溫泉",
MEMO_TIME: "各業者不同,依據現場公告",
POI: "Y",
file: "http://www.travel.taipei/d_upload_ttn/sceneadmin/pic/11000848.jpghttp://www.travel.taipei/d_upload_ttn/sceneadmin/pic/11002891.jpghttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C0/D315/E70/F65/1e0951fb-069f-4b13-b5ca-2d09df1d3d90.JPGhttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C0/D260/E538/F274/e7d482ba-e3c0-40c3-87ef-3f2a1c93edfa.JPGhttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C0/D919/E767/F581/9ddde70e-55c2-4cf0-bd3d-7a8450582e55.jpghttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C1/D28/E891/F188/77a58890-7711-4ca2-aebe-4aa379726575.JPG",
idpt: "臺北旅遊網",
latitude: "25.137077",
xbody: "北投溫泉從日據時代便有盛名,深受喜愛泡湯的日人自然不會錯過,瀧乃湯、星乃湯、鐵乃湯就是日本人依照溫泉的特性與療效給予的名稱,據說對皮膚病、神經過敏、氣喘、風濕等具有很好的療效,也因此成為了北部最著名的泡湯景點之一。新北投溫泉的泉源為大磺嘴溫泉,泉質屬硫酸鹽泉,PH值約為3~4之間,水質呈黃白色半透明,泉水溫度約為50-90℃,帶有些許的硫磺味 。目前北投的溫泉旅館、飯店、會館大部分集中於中山路、光明路沿線以及北投公園地熱谷附近,總計約有44家,每一家都各有其特色,多樣的溫泉水療以及遊憩設施,提供遊客泡湯養生,而鄰近的景點也是非常值得造訪,例如被列為三級古蹟的三寶吟松閣、星乃湯、瀧乃湯以及北投第一家溫泉旅館「天狗庵」,都有著深遠的歷史背景,而北投公園、北投溫泉博物館、北投文物館、地熱谷等,更是遊客必遊的景點,來到北投除了可以讓溫泉洗滌身心疲憊,也可以順便了解到北投溫泉豐富的人文歷史。",
_id: 1,
avEnd: "2016/07/07",
address: "臺北市 北投區中山路、光明路沿線"
},
{
info: "捷運站名:雙連站,轉乘紅33(固定班次)於大稻埕碼頭站下車。公車:9、206、274、641、669、704至大稻埕碼頭站及255、518、539至民生西路口站,再沿民生西路底方向步行約10分鐘抵達。 開車:沿著環河北路依大稻埕碼頭入口指引便可抵達。",
stitle: "大稻埕碼頭",
xpostDate: "2015/12/09",
longitude: "121.508274",
REF_WP: "10",
avBegin: "2008/08/02",
langinfo: "10",
MRT: "雙連",
SERIAL_NO: "2011051800000007",
RowNumber: "2",
CAT1: "景點",
CAT2: "藍色公路",
MEMO_TIME: "平常日以團體預約包船為主,例假日行駛固定航次,請洽詢各船公司。 強烈季風、漲退潮水位差影響航行及靠泊安全,當日實際航班得由現場公告或網站預告調整。",
POI: "Y",
file: "http://www.travel.taipei/d_upload_ttn/sceneadmin/pic/11000340.jpghttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C0/D906/E6/F186/809f30db-7079-421f-a625-7baa8ec21874.JPGhttp://www.travel.taipei/d_upload_ttn/sceneadmin/pic/11000341.jpghttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C0/D878/E420/F173/04765739-d40f-4d13-b271-8d5f9e5f44bd.JPGhttp://www.travel.taipei/d_upload_ttn/sceneadmin/pic/11000342.jpghttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C0/D20/E983/F199/866b5059-8fd7-4719-964c-51d2f78675d5.jpghttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C0/D546/E538/F353/ed2464d1-bc28-4790-96cd-5216db2c14f5.JPGhttp://www.travel.taipei/d_upload_ttn/sceneadmin/image/A0/B0/C1/D814/E111/F733/aed9d34d-890c-49fd-83ca-f76f38e4b94b.jpghttp://www.travel.taipei/streams/sceneadmin/video/100C1.mp3",
idpt: "臺北旅遊網",
latitude: "25.056847",
xbody: "大稻埕原是平埔族的居住地,因萬華(艋舺)同安人發生激烈的械鬥,造成族人移至大稻埕定居,開始大稻埕淡水河旁商店和房屋的興建,淡水港開放後,大稻埕在劉銘傳的治理下成為臺北城最繁華的物資集散中心,當中以茶葉、布料為主要貿易交易,當時的延平北路及貴德街一帶便是商業活動的重心,也讓大稻埕早年的歷史多采多姿、令人回味。 ",
_id: 2,
avEnd: "2015/12/09",
address: "臺北市 大同區環河北路一段"
}
]}}

自己在後方加入 ] 和 } 收尾 model就完成了 接下來我們來使用Retrofit吧!

 
public interface CallAttractionData {
    @GET("opendata/datalist/apiAccess?scope=resourceAquire&rid=36847f3f-deff-4183-a5bb-800737591de5")
    Call<AttractionsModel> getCall();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://data.taipei/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    CallAttractionData apiService = retrofit.create(CallAttractionData.class);

}
CallAttractionData
這邊講解一下
這邊是你在MainActivity.java的呼叫使用方式 retrofit.create(XXXX.class).getCall().enqueue(new CallBack{....})
XXXX.class 為你寫@GET的地方
以這篇文章為例 就會是CallAttractionData

 再來看看URL 網址:
https://data.taipei/opendata/datalist/apiAccess?scope=resourceAquire&rid=36847f3f-deff-4183-a5bb-800737591de5
通常一家公司的url前方都是固定的,像是台北市的url,開頭都會是
https://data.taipei/
所以GET內就是裝固定url後方的變數
此時拿台北市的例子來說就是
看起來就是上面CallAttractionData的樣子了

 如何在MainActivity.java使用retrofit 如同上述說的 retrofit.create(XXXX.class).getCall().enqueue(new CallBack{....})


設一個Log來看看吧
確定有獲得參數 retrofit有幫你獲取了json


再來看看整個MainActivity.java
 
public class MainActivity extends AppCompatActivity {

    private TextView text;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        text = findViewById(R.id.showtext);

        CallAttractionData.apiService.getCall().enqueue(new Callback<AttractionsModel>() {
            @Override
            public void onResponse(Call<AttractionsModel> call, @NonNull Response<AttractionsModel> response) {
                if (!response.isSuccessful())
                    return;

                AttractionsModel model = response.body();

                Log.d("MainActivity", "main = " + model.getResult().getCount());
                StringBuffer sb = new StringBuffer();
                for (int i = 0; i < model.getResult().getCount(); i++) {
                    sb.append("景點: - " + model.getResult().getResults().get(i).getAttractionTitle() + "\n");
                    sb.append("介紹: - " + model.getResult().getResults().get(i).getTourist() + "\n");
                    sb.append("-------------------------- \n");
                    text.setText(sb);
                }
            }

            @Override
            public void onFailure(Call<AttractionsModel> call, Throwable t) {

            }
        });

    }
}
xml畫面單純用scrollview包住
activity_main.xml
activity_main.xml
 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/showtext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


    </ScrollView>

</RelativeLayout>


run一下吧!
畫面會長得跟一開始我貼的gif一樣



總結:

之後再來講Observer觀察者模式

github code :
https://github.com/nikeru8/TaipeiTourist

2018年12月9日 星期日

Java基礎:for and foreach(加強版迴圈)

前言:

最近接到一個案子
處理資料比較重,邏輯上還必須使用到雙迴圈。
但大概才3000多筆資料而已
在雙迴圈+大量資料的情況下,用普通的for迴圈一秒居然才跑四筆....
這真的很讓人無言
原本有嘗試使用多執行緒去分割資料,分開讀取。
但效果還是和預想的差很多,畢竟一秒只跑四筆資料

最後我找到的解決方式 foreach

拿三千筆數量下去計算:
普通for要跑750s
foreach 1s結束

處理起來普通版for迴圈跟樹懶一樣讓人無言

實作:
List StringList = new ArrayList<>();
//用普通版迴圈 創建資料
for (int i = 0; i < 10000; i++) {
    StringList.add("比對資料 - " + i);
}

//普通版迴圈
for (int i = 0; i < StringList.size(); i++) {
    Log.d("CHECKPOINT", "check i " + StringList.get(i));
}

//Foreach 加強版回圈
for (String item : StringList) {
    Log.d("CHECKPOINT", "check i " + item);
}


為什麼會比較快呢?

普通for迴圈就是照著開始設定的 i 一個一個下去跑

foreach是用『遍歷』的方式下去跑

差在哪呢?

何謂遍歷?

一張圖了解差別
foreach遍歷
遍歷
普通for迴圈



結論:


當處理的資料量很大很大時,foreach超級強!!

資料量小,很容易就跑完,反而普通版的迴圈效能會比較好

可以想像成
你想運算的東西如果是工廠
普通版For就是一條生產線
Foreach是一間工廠多開很多條生產線
如果你運算量很小,你還要多花效能去多開生產線,告訴工人要做什麼,很不划算。
反而拖累整間工廠的運作


補充:
如果要再foreach遍歷的迴圈內計算count

可以使用這種方式
//Foreach 加強版回圈
int count = 0;
for (String item : StringList) {
    Log.d("CHECKPOINT", "check i " + item + "count - " + count);
    count++;
}





2018年11月14日 星期三

Java基礎:抽象abstract 和 介面interface




抽象方法abstract 和 介面interface

直接進入正題

abstract可以解釋成“一定會有的東西,所以只能繼承一次。
interface可以解釋成“附屬品”,可有可無,但有大量的東西都需要它,他就派上用場了。

舉實際的例子:武器
武器都一定有攻擊力、攻擊範圍、名字

這時候如果你需要大量的使用到武器,那上述說的『攻擊力、攻擊範圍、名字』這些東西就適合放在abstract。
但武器百百種,可能長槍會有紅櫻、刀和劍會有劍柄而且也可能會有紅櫻、長弓、短弓和十字弓會有
這些東西就不是一定會存在武器內的東西,我們就可以使用interface




實作:
先把傷害、攻擊範圍、武器名稱寫成abstract
武器.java

abstract public class 武器 {
    abstract int damage();

    abstract int range();

    abstract String getName();
}
這樣武器的基底,就出現了。
來製作武器吧
先來做一把霸王槍
霸王槍.java

public class 霸王槍 extends 武器 {

    @Override
    int damage() {
        return 200;
    }

    @Override
    int range() {
        return 80;
    }

    @Override
    String getName() {
        return getClass().getSimpleName();
    }

}

一把霸王槍就誕生了,他同時擁有了傷害、攻擊範圍、和名稱
再來創建一把青龍偃月刀配戴紅櫻

武器紅櫻.java
interface 紅纓 {

    int 裝配紅櫻();
}

然後是青龍偃月刀.java
public class 青龍偃月刀 extends 武器 implements 紅櫻 {
    @Override
    public int damage() {
        return 250;
    }

    @Override
    protected int range() {
        return 90;
    }

    @Override
    protected String getName() {
        return getClass().getSimpleName();
    }

    @Override
    public String 紅櫻樣式() {
        return "大花樣,關羽專用";
    }
}

這樣就區分了青龍偃月刀和霸王槍之間的差別。
再來
每把長槍,希望青龍偃月刀比較不同,那就給他一個顏色吧
武器顏色.java
public interface 武器配色 {
    String 顏色();
}

在改動青龍偃月刀
public class 青龍偃月刀 extends 武器 implements 紅櫻,武器配色 {
    @Override
    public int damage() {
        return 250;
    }

    @Override
    protected int range() {
        return 90;
    }

    @Override
    protected String getName() {
        return getClass().getSimpleName();
    }

    @Override
    public String 紅櫻樣式() {
        return "大花樣,關羽專用";
    }

    @Override
    public String 顏色() {
        return "青龍色";
    }
}


有注意到了嗎?
武器的是每一把、每一種武器都能套用的,但介面的部分可以一直增加。

此時我在創造一把武器,干將劍

我要有刀柄和紅櫻
干將劍.java
public class 干將劍 extends 武器 implements 武器刀柄,紅櫻{
    @Override
    public int damage() {
        return 170;
    }

    @Override
    protected int range() {
        return 10;
    }

    @Override
    protected String getName() {
        return getClass().getSimpleName();
    }

    @Override
    public int 刀柄長度() {
        return 5;
    }

    @Override
    public String 紅櫻樣式() {
        return "春秋古代款";
    }
}


一把佩戴紅纓的春秋款干將就誕生了

大致上abstract 和 interface的用法就是這樣

在寫個人使用他們吧:
我招喚出
張無忌.java
public class 張無忌 {

    private 武器 weapon;

    public void attack() {
        if (weapon == null) {
            System.out.println("張無忌空手,使用拳頭,造成敵人十點傷害");
            return;
        }
        System.out.println("張無忌使用 " + weapon.getName() + ",造成敵人" + weapon.damage() + "傷害");
    }

    public void 換武器(武器 weapon) {
        this.weapon = weapon;
    }

}


之後再MainActivity.java內來操作。
MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        張無忌 person = new 張無忌();

        person.attack();

        武器 weapon = new 干將劍();
        person.換武器(weapon);
        person.attack();
    }

}





2018年6月28日 星期四

Anroid元件(LocalBroadcastManager) — fragment刷新

前言:



fragment刷新對很多人來說是個坑。

不管fragment刷新 fragment
還是fragment 刷新 activity
或是activity 刷新 fragment

都可以用,萬用

有人應該會有疑問,這不是廣播嗎?
如果我參數設的跟別人一樣,不就會喚起我這個app了?

但仔細看他的元件名稱前方有個Local,應該就不擔心了吧。他只在你app內運作。
當你銷毀fragment 或是 activity時,註銷它就可以了。

開始之前如果對FragmentPagerAdapter、ViewPager不熟,可以先參考一下這篇。
http://nikeru8.blogspot.com/2017/11/androidfragmentpageradapterviewpager.html


重點程式碼:


 //參數
    protected LocalBroadcastManager broadcastManager;


 //接收廣播 (信件 然後你要 do something)
    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, Intent intent) {
            // 想像成從遠方寄來的信,可以是任何東西
            String action = intent.getStringExtra("changeSomething");
            if ("changeText".equals(action)) {
                new Handler().post(new Runnable() {
                    @Override
                    public void run() {
                        //do something
                        text_change.setText("你按了我~!!我刷新囉!");
                    }
                });
            }
        }
    };

    //註冊廣播 (你家的信箱)
    private void registerReceiver() {
        broadcastManager = LocalBroadcastManager.getInstance(getActivity());
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("changeFragment");
        broadcastManager.registerReceiver(mReceiver, intentFilter);
    }


    //呼叫刷新畫面(寄件人)
    private void refreshData() {
        //指定要刷新的頁面給intent (
        Intent intent = new Intent("changeFragment");
        //要帶過去的參數
        intent.putExtra("changeSomething", "changeText");
        LocalBroadcastManager.getInstance(activity).sendBroadcast(intent);
    }


     //離開fragment後,消滅廣播 (搬家後,信箱地址換掉)
     @Override
    public void onDetach() {
        super.onDetach();
        broadcastManager.unregisterReceiver(mReceiver);
    }

    //註冊廣播 (蓋成房子後,買一個信箱) 
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        registerReceiver();
    }


 

這邊解釋一下,可以想像成fragment是一棟房子,你要寄信過去。
房子-fragment
信箱-registerReceiver()
寄件人-refreshData()
信件-mReceiver

有了房子(fragment)就必須有信箱(registerReceiver())才能收信,所以要在房子內蓋一個信箱。

當寄件人(refreshData())寄出了信,就會投地到信箱(registerReceiver()),然後你可以開啟信件(mReceiver)收到任何東西、做任何事。

夠簡單吧。


就不全部貼出來了,直接看代碼吧。


github:https://github.com/nikeru8/refreshFragment



2018年4月23日 星期一

Anadroid元件(Camera) — 客製化相機、使用相機的正確姿勢(二)

前言:

證件照相機



有一種相機,叫做證件相機!

這邊提供客製化相機的做法。

在網路上找資料很多,但一直沒有確切可以用的code。

不是不完整,不然就是blog寫的當下android版本太低已不適用現今環境。


發現網路還是有一篇能用的。

 https://blog.csdn.net/LHBTM/article/details/55505668

這篇不完整,但都把釣竿給你了。

如果你整篇複製貼上,你會發現拍完照後,並不會返回MainActivity,就卡在拍照頁。

仔細看一下Code,其實說明都很完整!

合併一下官方文擋獲取儲存File位置的方式,來修改這篇。


先佈局好畫面:
acitvity_main.xml

activity_main.xml
 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hello.kaiser.customcamera.MainActivity">

    <ImageView
        android:id="@+id/show_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:src="@drawable/ic_launcher_background"
        android:text="Hello World!" />

    <Button
        android:id="@+id/btn_take_pic"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/show_image"
        android:layout_centerHorizontal="true"
        android:text="拍照" />

</RelativeLayout> 

然後MainActivity.java
 
public class MainActivity extends AppCompatActivity {

    private Button mBtnPic;
    private ImageView mShowImage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();


    }

    private void initView() {
        mBtnPic = (Button) findViewById(R.id.btn_take_pic);
        mShowImage = (ImageView) findViewById(R.id.show_image);
    }
}


沒啥功能。
接下來開始大家都很會的crtl+c crtl+v
https://blog.csdn.net/LHBTM/article/details/55505668
把上面這篇全部搬到你個人的專案內。

會動到的地方

會動到的地方,大概會長這樣

比較需要注意的,就是xmlView

記得換成自己包名的位置


記得更換View的名稱




記得在MainActivity.java製作畫面轉移事件
 mBtnPic.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, TakePicActivity.class));
            }
        }); 


此時,大致上完成了
開啟後你會發現,事情沒你想得順利XD

至少畫面出來了,是吧!




這像我上一篇講得不太一樣,上一篇甚至連權限都不用。

這邊因為是客製化,就必須加權限進來。


此時要加入三個權限:
寫入相機權限、寫入SDCard權限、和相機權限
 
    <!--寫入手機權限-->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!--相機權限-->
    <uses-permission android:name="android.permission.CAMERA" />

權限的部分就請參考
http://nikeru8.blogspot.tw/2017/04/android-permission.html


權限加入 MainActivity.java
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private Activity activity;
    public static final int PermissionCode = 1000;
    public static final int GetPhotoCode = 1001;

    private Button mBtnPic;
    private ImageView mShowImage;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        activity = this;
        initView();
        initListener();

    }

    private void initView() {
        mBtnPic = (Button) findViewById(R.id.btn_take_pic);
        mShowImage = (ImageView) findViewById(R.id.show_image);
    }

    private void initListener() {
        mBtnPic.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                //檢查是否取得權限
                final int permissionCheck = ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA);

                //沒有權限時
                if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.CAMERA,
                                    Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS,
                                    Manifest.permission.WRITE_EXTERNAL_STORAGE},
                            PermissionCode);
                } else { //已獲得權限
                    Toast.makeText(activity, "已經拿到權限囉!", Toast.LENGTH_SHORT).show();
                    startActivityForResult(new Intent(MainActivity.this, TakePicActivity.class), GetPhotoCode);
                }

            }
        });
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == PermissionCode) {
            //假如允許了
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //do something
                Toast.makeText(this, "感謝賜予權限!", Toast.LENGTH_SHORT).show();
                startActivityForResult(new Intent(MainActivity.this, TakePicActivity.class), GetPhotoCode);
            }
            //假如拒絕了
            else {
                //do something
                Toast.makeText(this, "CAMERA權限FAIL", Toast.LENGTH_SHORT).show();
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}



此時,你按相機,已經會出現畫面了吧!

但按下去的時候,他會頓一下(已經拍照ing)然後...就沒有然後了。

為什麼呢,其實已經有拍照的效果,只是沒有把畫面帶回MainActivity內而已
此時就要往takePicture這方法追到底發生什麼事情

仔細看一下作者寫的 CameraSurfaceView 會發現
 
 public void takePicture(){
        //设置参数,并拍照
        setCameraParams(mCamera, mScreenWidth, mScreenHeight);
        // 当调用camera.takePiture方法后,camera关闭了预览,这时需要调用startPreview()来重新开启预览
        mCamera.takePicture(null,null, jpeg);
    }

作者在這邊只拍了照,卻沒有結束掉finish();
這邊可以自己加入finish();

我們這邊設計,你按拍照後...
一、檢查相機權限。
二、帶畫面。(創建相片存放位置FilePath並且把filePath帶到拍照頁面的activity,拍完照後存入到filePath的位置)


上面已經增加了權限。
這邊我們來帶畫面吧
 在MainActivity先把要存照片的地方位置創建好。
 
 //創造檔案名稱、和存擋路徑
     String imageFilePath;

    private File createImageFile() throws IOException {
        String timeStamp =
                new SimpleDateFormat("yyyyMMdd_HHmmss",
                        Locale.getDefault()).format(new Date());
        String imageFileName = "IMG_" + timeStamp + "_";
        File storageDir =
                getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        File image = File.createTempFile(
                imageFileName,  /* prefix */
                ".jpg",         /* suffix */
                storageDir      /* directory */
        );


        imageFilePath = image.getAbsolutePath();
        return image;
    }

此時,我們就獲取到創建的File


整理一下MainActivity

把權限和帶畫面的Code整理在同一個方法內

MainActivity
 
public class MainActivity extends AppCompatActivity {

    private Activity activity;
    public static final int PermissionCode = 1000;
    public static final int GetPhotoCode = 1001;

    private Button mBtnPic;
    private ImageView mShowImage;
    String imageFilePath;

    private boolean isCameraPermission = false;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        activity = this;
        initView();
        initListener();

    }

    private void initView() {
        mBtnPic = (Button) findViewById(R.id.btn_take_pic);
        mShowImage = (ImageView) findViewById(R.id.show_image);
    }

    private void initListener() {
        mBtnPic.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                openCamera();
            }
        });
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == PermissionCode) {
            //假如允許了
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                isCameraPermission = true;
                //do something
                Toast.makeText(this, "感謝賜予權限!", Toast.LENGTH_SHORT).show();
                startActivityForResult(new Intent(MainActivity.this, TakePicActivity.class), GetPhotoCode);
            }
            //假如拒絕了
            else {
                isCameraPermission = false;
                //do something
                Toast.makeText(this, "CAMERA權限FAIL,請給權限", Toast.LENGTH_SHORT).show();
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    //創造檔案名稱、和存擋路徑
    private File createImageFile() throws IOException {
        String timeStamp =
                new SimpleDateFormat("yyyyMMdd_HHmmss",
                        Locale.getDefault()).format(new Date());
        String imageFileName = "IMG_" + timeStamp + "_";
        File storageDir =
                getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        File image = File.createTempFile(
                imageFileName,  /* prefix */
                ".jpg",         /* suffix */
                storageDir      /* directory */
        );

        imageFilePath = image.getAbsolutePath();
        return image;
    }

    private void openCamera() {
        //已獲得權限
        if (isCameraPermission) {
            File photoFile = null;
            try {
                photoFile = createImageFile();
            } catch (IOException e) {
                Log.d("checkpoint", "error for createImageFile 創建路徑失敗");
            }
            //成功創建路徑的話
            if (photoFile != null) {
                Intent intent = new Intent(MainActivity.this, TakePicActivity.class);
                Bundle bundle = new Bundle();
                bundle.putString("url", photoFile.getAbsolutePath());
                intent.putExtras(bundle);
                startActivityForResult(intent, GetPhotoCode);
            }
        }
        //沒有獲得權限
        else {
            getPermission();
        }
    }

    private void getPermission() {
        //檢查是否取得權限
        final int permissionCheck = ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA);
        //沒有權限時
        if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
            isCameraPermission = false;
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.CAMERA,
                            Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    PermissionCode);
        } else { //已獲得權限
            isCameraPermission = true;
            openCamera();
        }
    }

}



再來就會動到原作者的Code了
CameraSurfaceView
 

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.FrameLayout;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;

/**
 * Created by Administrator on 2017/2/15 0015.//自定义相机
 */
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Camera.AutoFocusCallback {

    private static final String TAG = "CameraSurfaceView";

    private Context mContext;
    private SurfaceHolder holder;
    private Camera mCamera;

    private int mScreenWidth;
    private int mScreenHeight;
    private CameraTopRectView topView;

    //更動
    private String filePath;
    private Activity activity;

    public CameraSurfaceView(Context context) {
        this(context, null);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        getScreenMetrix(context);
        topView = new CameraTopRectView(context, attrs);

        initView();


    }

    //拿到手机屏幕大小
    private void getScreenMetrix(Context context) {
        WindowManager WM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        WM.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;
        mScreenHeight = outMetrics.heightPixels;

    }

    private void initView() {
        holder = getHolder();//获得surfaceHolder引用
        holder.addCallback(this);
//        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);//设置类型

    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.i(TAG, "surfaceCreated");
        if (mCamera == null) {
            mCamera = Camera.open();//开启相机
            try {
                mCamera.setPreviewDisplay(holder);//摄像头画面显示在Surface上
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Log.i(TAG, "surfaceChanged");

        setCameraParams(mCamera, mScreenWidth, mScreenHeight);
        mCamera.startPreview();
//        mCamera.takePicture(null, null, jpeg);

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.i(TAG, "surfaceDestroyed");
        mCamera.stopPreview();//停止预览
        mCamera.release();//释放相机资源
        mCamera = null;
        holder = null;
    }

    @Override
    public void onAutoFocus(boolean success, Camera Camera) {
        if (success) {
            Log.i(TAG, "onAutoFocus success=" + success);
            System.out.println(success);
        }
    }


    private void setCameraParams(Camera camera, int width, int height) {
        Log.i(TAG, "setCameraParams  width=" + width + "  height=" + height);
        Camera.Parameters parameters = mCamera.getParameters();
        // 获取摄像头支持的PictureSize列表
        List pictureSizeList = parameters.getSupportedPictureSizes();
        for (Camera.Size size : pictureSizeList) {
            Log.i(TAG, "pictureSizeList size.width=" + size.width + "  size.height=" + size.height);
        }
        /**从列表中选取合适的分辨率*/
        Camera.Size picSize = getProperSize(pictureSizeList, ((float) height / width));
        if (null == picSize) {
            Log.i(TAG, "null == picSize");
            picSize = parameters.getPictureSize();
        }
        Log.i(TAG, "picSize.width=" + picSize.width + "  picSize.height=" + picSize.height);
        // 根据选出的PictureSize重新设置SurfaceView大小
        float w = picSize.width;
        float h = picSize.height;
        parameters.setPictureSize(picSize.width, picSize.height);
        this.setLayoutParams(new FrameLayout.LayoutParams((int) (height * (h / w)), height));

        // 获取摄像头支持的PreviewSize列表
        List previewSizeList = parameters.getSupportedPreviewSizes();

        for (Camera.Size size : previewSizeList) {
            Log.i(TAG, "previewSizeList size.width=" + size.width + "  size.height=" + size.height);
        }
        Camera.Size preSize = getProperSize(previewSizeList, ((float) height) / width);
        if (null != preSize) {
            Log.i(TAG, "preSize.width=" + preSize.width + "  preSize.height=" + preSize.height);
            parameters.setPreviewSize(preSize.width, preSize.height);
        }

        parameters.setJpegQuality(100); // 设置照片质量
        if (parameters.getSupportedFocusModes().contains(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
            parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);// 连续对焦模式
        }

        mCamera.cancelAutoFocus();//自动对焦。
        mCamera.setDisplayOrientation(90);// 设置PreviewDisplay的方向,效果就是将捕获的画面旋转多少度显示
        mCamera.setParameters(parameters);

    }

    /**
     * 从列表中选取合适的分辨率
     * 默认w:h = 4:3
     * 注意:这里的w对应屏幕的height
     * h对应屏幕的width

*/
    private Camera.Size getProperSize(List pictureSizeList, float screenRatio) {
        Log.i(TAG, "screenRatio=" + screenRatio);
        Camera.Size result = null;
        for (Camera.Size size : pictureSizeList) {
            float currentRatio = ((float) size.width) / size.height;
            if (currentRatio - screenRatio == 0) {
                result = size;
                break;
            }
        }

        if (null == result) {
            for (Camera.Size size : pictureSizeList) {
                float curRatio = ((float) size.width) / size.height;
                if (curRatio == 4f / 3) {// 默认w:h = 4:3
                    result = size;
                    break;
                }
            }
        }

        return result;
    }


    // 拍照瞬间调用
    private Camera.ShutterCallback shutter = new Camera.ShutterCallback() {
        @Override
        public void onShutter() {
            Log.i(TAG, "shutter");
            System.out.println("执行了吗+1");
        }
    };

    // 获得没有压缩过的图片数据
    private Camera.PictureCallback raw = new Camera.PictureCallback() {

        @Override
        public void onPictureTaken(byte[] data, Camera Camera) {
            Log.i(TAG, "raw");
            System.out.println("执行了吗+2");
        }
    };

    //创建jpeg图片回调数据对象
    private Camera.PictureCallback jpeg = new Camera.PictureCallback() {

        private Bitmap bitmap;

        @Override
        public void onPictureTaken(byte[] data, Camera Camera) {


            topView.draw(new Canvas());

            BufferedOutputStream bos = null;
            Bitmap bm = null;
            if (data != null) {

            }

            try {
                // 获得图片
                bm = BitmapFactory.decodeByteArray(data, 0, data.length);
                Log.d("checkpoint", "checkpoint - " + bm);
//                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//                    String filePath = "/sdcard/dyk" + System.currentTimeMillis() + ".JPEG";//照片保存路径

//                    //图片存储前旋转
                Matrix m = new Matrix();
                int height = bm.getHeight();
                int width = bm.getWidth();
                m.setRotate(90);
                //旋转后的图片
                bitmap = Bitmap.createBitmap(bm, 0, 0, width, height, m, true);


                System.out.println("执行了吗+3");
                File file = new File(filePath);
                if (!file.exists()) {
                    file.createNewFile();
                }
                bos = new BufferedOutputStream(new FileOutputStream(file));

                Bitmap sizeBitmap = Bitmap.createScaledBitmap(bitmap,
                        topView.getViewWidth(), topView.getViewHeight(), true);
                bm = Bitmap.createBitmap(sizeBitmap, topView.getRectLeft(),
                        topView.getRectTop(),
                        topView.getRectRight() - topView.getRectLeft(),
                        topView.getRectBottom() - topView.getRectTop());// 截取


                bm.compress(Bitmap.CompressFormat.JPEG, 100, bos);//将图片压缩到流中

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    bos.flush();//输出
                    bos.close();//关闭
                    bm.recycle();// 回收bitmap空间
                    mCamera.stopPreview();// 关闭预览
                    activity.setResult(Activity.RESULT_OK);
                    activity.finish();
//                    mCamera.startPreview();// 开启预览
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    };

    public void takePicture(Activity activity, String filePath) {
        this.filePath = filePath;
        this.activity = activity;
        //设置参数,并拍照
        setCameraParams(mCamera, mScreenWidth, mScreenHeight);
        // 当调用camera.takePiture方法后,camera关闭了预览,这时需要调用startPreview()来重新开启预览
        mCamera.takePicture(null, null, jpeg);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

}





我主要把activityfilePath帶入這邊的takePicture方法。
註解掉做者獲取filePath的方法,帶入自己的filePath


並且增加activity關掉的finish()


再回到TakePicActivity,接收在MainActivity創建的filePath
 
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;

public class TakePicActivity extends AppCompatActivity {


    private Button button;
    private CameraSurfaceView mCameraSurfaceView;

    private Activity activity;
    String filePath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        activity = this;
        getBundleData();

        initSet();
        initView();

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mCameraSurfaceView.takePicture(activity, filePath);
            }
        });
    }

    private void initSet() {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // 全屏显示
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContentView(R.layout.activity_take_pic);
    }


    private void initView() {
        mCameraSurfaceView = (CameraSurfaceView) findViewById(R.id.cameraSurfaceView);
        button = (Button) findViewById(R.id.takePic);
    }

    private void getBundleData() {
        Bundle bundle = getIntent().getExtras();
        if (bundle != null) {
            filePath = bundle.getString("url");
        }
        Log.d("checkpoint", "check filePath - " + filePath);
    }
}


再回到MainActivity做一次整理
 多增加onActivityResult 和 自寫的方法setPic
 
 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == GetPhotoCode) {
            setPic(imageFilePath);
        }
    }

    private void setPic(String mCurrentPhotoPath) {
        // Get the dimensions of the View
        int targetW = mShowImage.getWidth();
        int targetH = mShowImage.getHeight();

        // Get the dimensions of the bitmap
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
        int photoW = bmOptions.outWidth;
        int photoH = bmOptions.outHeight;

        // Determine how much to scale down the image
        int scaleFactor = Math.min(photoW / targetW, photoH / targetH);

        // Decode the image file into a Bitmap sized to fill the View
        bmOptions.inJustDecodeBounds = false;
        bmOptions.inSampleSize = scaleFactor;
        bmOptions.inPurgeable = true;

        Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
        mShowImage.setImageBitmap(bitmap);
    }



就完成囉!

有仔細看的朋友一定會想知道...

原作者filePath寫得好好的為啥要動?

拍照會延遲,可能照片還沒拍好、圖片都還沒存到路徑TackPicActivity就被關掉了。

所以我才改動filePath的取得方式。


屁話講了那麼多,重點大家就是要程式碼吧 哈哈哈

Demo:
https://github.com/nikeru8/CustomCamera


如果有錯誤或者不懂的地方歡迎提問!


文獻:
https://developer.android.com/training/camera/photobasics.html#TaskScalePhoto
 https://blog.csdn.net/LHBTM/article/details/55505668

2018年4月12日 星期四

Android方法 — 使用相機的正確姿勢(一)

前言:






這裡來實作相機的使用。

7.0牛扎糖官方改過權限,所以造成7.0以下會有FileUriExposedException的風險

這邊讓你迎刃而解















文獻來自官方檔案:
https://developer.android.com/training/camera/photobasics.html#TaskScalePhoto


實作:

先講一下要動到的地方




先在AndroidManifest.xml內給權限吧
 
<manifest>
...
      <uses-feature android:name="android.hardware.camera"
                  android:required="true" />
...
</manifest>

在上述的程式中,android:required=”true”可以判斷使用者的手機裝置是否有相機。
假設我們改成false,我們就必須透過寫code的方式由我們自己去確認。

讓我們來開相機吧
處理開到系統相機Intent的部分
 
private static final int REQUEST_CAPTURE_IMAGE = 100;

private void openCameraIntent() {
    Intent pictureIntent = new Intent(
                              MediaStore.ACTION_IMAGE_CAPTURE
                           );
    if(pictureIntent.resolveActivity(getPackageManager()) != null) { 
           startActivityForResult(pictureIntent,
                              REQUEST_CAPTURE_IMAGE);
    }
}

我們再來處理拍完照後,回來App的畫面處理
 
@Override
protected void onActivityResult(int requestCode, int resultCode,
                                              Intent data) {
    if (requestCode == REQUEST_CAPTURE_IMAGE && 
                              resultCode == RESULT_OK) {
        if (data != null && data.getExtras() != null) {
        Bitmap imageBitmap = (Bitmap) data.getExtras().get("data");
        mImageView.setImageBitmap(imageBitmap);
        }
    }
}

簡單的拍照,返回照片就完成囉!!

下載Demo:https://github.com/nikeru8/CameraDemo/commits/master


但上面只是做好看的,因為並沒有『儲存』這個動作。

拿上面的程式碼拍完照後,會發現沒有在相簿裡出現!?

當然啊,你又沒寫XD

接下來你必須寫一個存入系統的方法

一樣先加入權限吧!
manifests內加入存入權限
 
我以為要存儲權限,但原生的相機貌似不用。(而且這篇我也忘了加入呼叫權限的code XD


現在讓我們創建一個方法,創立文件名稱“日期_檔名”,把拍下來的照片寫入外部的檔案目錄內。
 
String imageFilePath;
private File createImageFile() throws IOException {
    String timeStamp = 
         new SimpleDateFormat("yyyyMMdd_HHmmss", 
                      Locale.getDefault()).format(new Date());
    String imageFileName = "IMG_" + timeStamp + "_";
    File storageDir =  
                getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    File image = File.createTempFile(
                    imageFileName,  /* prefix */
                    ".jpg",         /* suffix */
                    storageDir      /* directory */
    );

    imageFilePath = image.getAbsolutePath();
    return image;
}



上面的方法已經寫好了圖片儲存的地點,接下來開始存圖片了。
把創建資料夾的路徑帶給相機,並開啟系統資料夾!
下面複寫了剛剛上面開啟相機的方式
 
private static final int REQUEST_CAPTURE_IMAGE = 100;

private void openCameraIntent() {
        Intent pictureIntent = new Intent(
                MediaStore.ACTION_IMAGE_CAPTURE);
        if (pictureIntent.resolveActivity(getPackageManager()) != null) {
            //創建一個資料夾去存圖片
            File photoFile = null;
            try {
                photoFile = createImageFile();
            } catch (IOException ex) {
                //當創建資料夾失敗...
            }

            //當創建的資料夾不為null直,把創建資料夾的路徑帶給相機,並開啟系統資料夾
            if (photoFile != null) {
                Uri photoURI = FileProvider.getUriForFile(this, "com.example.android.provider", photoFile);
                pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                        photoURI);
                startActivityForResult(pictureIntent,
                        REQUEST_CAPTURE_IMAGE);
            }
        }
    }


仔細看一下上述的程式碼,你一定會發現!“com.example.android.provider"是哪來的?在API等級 24以上的手機,都需要使用FileProvider去和你app內的manifest File 做連結。

讓我們開始FileProvider的步驟吧!
manifest內加入provider 
 
<application>
   ...
   <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths">
        </meta-data>
    </provider>
    ...
</application>

發現${applicationId}.provider 了嗎?這個就是串聯剛剛的“com.example.android.provider"

直接把 ${applicationId}改成你App的Id名稱,這邊拿我的專案當範例


這兩邊的名稱要是一樣的,之後會長這樣
android:authorities="com.hello.kaiser.startcamera.provider"

發現紅色的地方了嗎?
“@xml/file_paths”
這邊就要自己創建了。
資料夾創建在res下面
變成


之後創建名為file_paths的xml檔案
 
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images"
        path="Android/data/com.example.package.name/files/Pictures" />
</paths>

上述程式碼中的path對應的就是剛剛上面寫得codegetExternalFilesDir()』的部分,也是你存到指定環境Environment.DIRECTORY_PICTURES的地方。

所以path必須做更改。

同樣拿我的專案舉例子,path就會改成
path="Android/data/com.hello.kaiser.startcamera/files/Pictures" 
長這樣子。

就差不多完成囉!

此時,拍完照片 > 返回你的app,就剩下顯示影像的工作了。

是寫在onActivityResult的地方!
onActivityResult(int requestCode, int resultCode, Intent data)
但又跟我們一開始寫的方式不一樣,此時帶回來的data會為nulll,所以判斷onActivityResult的部分只需要判斷requestCode是否為我們剛剛帶入的REQUEST_CAPTURE_IMAGE就行了,剛剛在上面提過獲取了imageFilePath路徑的方法。

現在直接在onActivityReslut內調用
 
@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CAPTURE_IMAGE && resultCode == RESULT_OK) {
            setPic();
        }
    }

    private void setPic() {
        // 獲取你在actiivity_layout地方的ImageView大小
        int targetW = mImageView.getWidth();
        int targetH = mImageView.getHeight();

        //獲取剛剛拍照圖片的大小
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(imageFilePath, bmOptions);
        int photoW = bmOptions.outWidth;
        int photoH = bmOptions.outHeight;

        //修改你要顯示圖片的尺寸大小
        int scaleFactor = Math.min(photoW / targetW, photoH / targetH);

        //使用Bitmap size去調整ImageView內顯示的圖片
        bmOptions.inJustDecodeBounds = false;
        bmOptions.inSampleSize = scaleFactor;
        bmOptions.inPurgeable = true;
        Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath, bmOptions);

        //顯影像
        mImageView.setImageBitmap(bitmap);
    }

可以開啟你的程式,試著拍照吧。
會存在手機裡面囉!

onActivityReslut內可以使用更簡潔的方式,使用Glide

直接
 
Glide.with(this).load(imageFilePath).into(mImageView);

Glide介紹及使用方式:
http://nikeru8.blogspot.tw/2017/03/third-party-frescopicasso.html
官方:
https://github.com/bumptech/glide

需要注意的事情是,目前的第三方套件,套用方式已經改變
請在你的Gradle內加入
 
repositories {
  mavenCentral()
  google()
}

dependencies {
  implementation 'com.github.bumptech.glide:glide:4.7.1'
  annotationProcessor 'com.github.bumptech.glide:compiler:4.7.1'
}

這是在android3.0之後的改變

有問題請提出吧。

你的問題會是我創作很大的動力!

Demo:https://github.com/nikeru8/CameraDemo


心得:

之後還寫了一篇
客製化相機(二) 關於證件照的。

請參閱囉



協程(coroutine) - 協程為什麼要學它?

 Coroutine 協程 再強調一次 協程就是由kotlin官方所提供的線程api //Thread Thread { }.start() //Executor val execu...