Android CameraX Bitmap 繪製後照片方向錯誤

問題說明 在使用 CameraX API 時,如果有在相機初始時實作下列程式碼,原則上API 會根據目前手機的方向來自動轉正相片。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 控制照片轉正 val orientationEventListener = object : OrientationEventListener(this) { override fun onOrientationChanged(orientation: Int) { val rotation: Int = when (orientation) { in 45..134 -> Surface.ROTATION_270 // 頭朝右 in 135..224 -> Surface.ROTATION_180 // 頭朝下 in 225..314 -> Surface.ROTATION_90 // 頭朝左 else -> Surface.ROTATION_0 // 頭朝上 } this@MainActivity.rotation = rotation imageCapture!!.targetRotation = rotation } } orientationEventListener.enable() 但是,當我們需要在輸出的照片上繪製文字或圖案時,就會遇到讀入的Bitmap 都是預設的橫向,而當我們需要繪製的時候,就會有長寬座標錯置的問題。 解決方式 必須把照片轉兩次。 需要轉兩次的原因在於,第一次的轉正是為了讓我們能夠根據使用者的角度,去繪製我們需要繪製的圖案或文字,但當繪製完成後,在輸出之前必須再將照片轉回原始讀入時的橫向,讓電腦端能夠用EXIF來自動轉正相片。 轉正跟轉回,可以試著拿張名片或紙自己轉轉看就會懂了。 實作的程式碼如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 companion object { private const val ROTATION_HEAD_UP = 0 private const val ROTATION_HEAD_LEFT = 1 private const val ROTATION_HEAD_DOWN = 2 private const val ROTATION_HEAD_RIGHT = 3 } private fun processPictureFile(photoFile: File) { // 建立 Bitmap的設定 val bitmapOptions = BitmapFactory.Options() bitmapOptions.inPurgeable = true bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565 bitmapOptions.inDither = true bitmapOptions.inMutable = true // 避免Canvas物件建立時picBitmap為immutable // 1. 旋轉照片 val mat = Matrix() when (rotation) { ROTATION_HEAD_UP -> mat.postRotate(90f) // 手機上端朝右時,照片會上下顛倒,需特別處理轉正。 ROTATION_HEAD_RIGHT -> mat.postRotate(180f) ROTATION_HEAD_DOWN -> mat.postRotate(270f) ROTATION_HEAD_LEFT -> mat.postRotate(0f) } try { // 讀入原始相片 val inputStream = ByteArrayInputStream(photoFile.readBytes()) // 原始相片轉成 Bitmap val bitmap = BitmapFactory.decodeStream(inputStream, null, bitmapOptions) ?: return Log.d(TAG, "processPictureFile: bitmap.width ${bitmap.width}") Log.d(TAG, "processPictureFile: bitmap.height ${bitmap.height}") // 複製一份要轉方向的 Bitmap val picBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, mat, true) // 建立畫布 val canvas = Canvas(picBitmap) // 繪製在畫布上 canvas.drawBitmap(bitmap, mat, null) // TODO: 執行你需要做的繪製行為... // 設定轉回去的矩陣 val restoreMat = Matrix() when (rotation) { ROTATION_HEAD_UP -> restoreMat.postRotate(270f) ROTATION_HEAD_RIGHT -> restoreMat.postRotate(180f) ROTATION_HEAD_DOWN -> restoreMat.postRotate(90f) ROTATION_HEAD_LEFT -> restoreMat.postRotate(0f) } // 執行轉回去 val restoreBitmap = Bitmap.createBitmap(picBitmap, 0, 0, picBitmap.width, picBitmap.height, restoreMat, true) val restoreCanvas = Canvas(restoreBitmap) restoreCanvas.drawBitmap(restoreBitmap, mat, null) // TODO: 輸出... } catch (e: java.lang.Exception) { Log.e(TAG, "processPictureFile: ", e) } } 補充: 相片轉正的邏輯 對 Android 手機而言,頭朝左的水平方向(landscape),才是正常的方向,所以預設讀取的圖片都會是橫向。 因此直向拍照等方式都需要轉向。 ...

Published on March 25, 2022 · 2 min · 361 words · Daniel Huang

Android 常用 ADB 指令

裝置 列出裝置清單 1 adb devices 取得裝置 IP 1 2 adb shell $ ip -f inet addr show wlan0 啟用裝置 port 1 adb -s {deviceId} tcpip {port} 透過Wi-Fi連結裝置 1 adb -s {deviceId} connect {ip:port} 斷開連結 1 adb disconnect 安裝apk 全新安裝 在xxxx.apk的根目錄下執行cmd 1 adb -s {deviceId} install {xxxxx.apk} 覆蓋舊版 1 adb -s {deviceId} install -r {xxxx.apk} 裝置控制 點亮螢幕 1 adb shell input keyevent KEYCODE_POWER 參考資料 Day 8 - 常用 adb 指令及實用小技巧 如何透過 adb command line 指令啟動 Android App

Published on November 17, 2021 · 1 min · 76 words · Daniel Huang

Android 開發 | 加密版的 SharedPreference - EncryptedSharedPreferences

介紹 根據官方文件, EncryptedSharedPreferences 使用 2-part system 來管理金鑰,並用金鑰加解密儲存的資料。 一組 Keyset(密鑰集合)包含一個或多個 Key(密鑰),用來加密解密資料。這組 Keyset 會儲存在 SharedPreferences 裡。 一把 Primary Key (主鑰) 負責加密所有的 Keysets,這把是存在 Android 的 KeyStore 裡面。 運作流程 graph TD; A[應用程式存取 EncryptedSharedPreferences] --> B{檢查 MasterKey 是否存在於 KeyStore} B -- 存在 --> C[讀取 MasterKey] B -- 不存在 --> D[建立新的 MasterKey] C --> E{檢查 KeySet 是否存在於 SharedPreferences} D --> E E -- 存在 --> F[讀取 KeySet] E -- 不存在 --> G[產生新的 KeySet 並加密存入 SharedPreferences] F --> H[使用 KeySet 加密 Key & Value] G --> H H --> I[存入 shared_prefs/*.xml 已加密的數據] 資源引用 1 2 3 4 5 6 7 8 9 10 11 // Encrypted SharedPreference implementation "androidx.security:security-crypto:1.0.0" // For Identity Credential APIs implementation "androidx.security:security-identity-credential:1.0.0-alpha03" // For App Authentication APIs implementation "androidx.security:security-app-authenticator:1.0.0-alpha02" // For App Authentication API testing androidTestImplementation "androidx.security:security-app-authenticator:1.0.0-alpha02" 這個套件基本上也可以對檔案加密,但本篇以實作 SharedPerefrences 為主,詳情可以參考 Work with data more securely。 ...

Published on October 8, 2021 · 3 min · 558 words · Daniel Huang

2021 Associate Android Developer 考試心得

📢 2024/08/26 更新 Google 已不再接受新的考試報名,看來也沒有要再開新的 Android 認證了😢。 前言 其實考 AAD 已經想了很久,從當初踏入 Android 這個領域,就有計畫在工作的第二年或第三年要考。 但是礙於工作跟各種推託的原因,卻都沒有很認真的去計劃這項考試的時間。 終於在 2021 年的三月底,趕在手上的護照過期前,狠下心把信用卡給他刷下去,報名了 AAD 的 Java 版。 (嗚嗚,我的4200多塊台幣 QAQ。) 這裡必須說明一下,如果您還在猶豫要選擇哪一個版本的話,我會建議選 Kotlin 的版本去考。 畢竟 Kotlin 是官方主推的語言,而且也是目前市場上較為搶手的技能,選 Kotlin 的 CP 值還是比較高一些。 但是,為什麼我還是選 Java 呢? 因為,實在是對 Kotlin 還不夠熟悉,怕這錢付下去就真的是繳學費而已… 第一步: 報名 報名連結 打開報名網站,裡面除了考試規則與費用的說明,也有關於考試內容的學習指南。 ==強烈建議在考之前去看看學習指南==裡的資訊,確認一下裡面有出現的相關技能或套件都有使用過。 因為以我這次考的經驗,基本上學習指南裡有的,考試都有考出來。 確定報名之後,會被導到 TrueAbility 的服務。 需要填以下這些資訊,以英文書寫,照著步驟一步一步走,不會太困難。 上傳護照與自拍照 填寫證書上的姓名與聯絡地址等資料 填寫付款信用卡資料 證件審核大概會花一到兩小時的時間,審核完成後才會在畫面上看到可以考試的按鈕。審核完不一定要馬上考試,可以挑自己有空的時間。 第二步: 正式考試 證件審核通過後,就可以按下按鈕考試啦~ 首先會先看到一份說明網頁,說明完整的考試流程與規則。 重點大概有下列幾點: 安裝最新版本的 Android Studio 與 SDK,並下載考試專用的 Plugin。 考試時間共 8 小時,時間到了系統會自動上傳。 (不含試後錄影答題的時間) 考試期間必須保持連網。 考試期間是可以 Google 找答案的。 切勿與他人合作考試。 錄影答題沒完成視同考試失敗。 安裝完環境與 Plugin 後,按下 Android Studio 上的考試鈕,就可以開始考試了。 基本上考試的內容就是要完成一個專案,會提供你這個案子的規格與必須要完成的任務。 在 Android Studio 上也會有顯示目前剩餘的時間還有多少,提早完成也可以提早送出。 ...

Published on March 31, 2021 · 1 min · 154 words · Daniel Huang

Android 開發 | 歡迎畫面 Splash Screen

一、前言 歡迎畫面會是使用者打開 App 時,第一個看到的畫面,畫面顯示的時間長度,會根據 App 啟動的速度而有所不同。 在 Android 11 以前,歡迎畫面通常都是白色,但在 Android 12 以後,系統會使用 App Icon 與 App theme 中設定的 windowBackground 製作 App 的歡迎畫面。 如果我們要客製化 App 的歡迎畫面,該怎麼做呢? 二、常見的幾個老方法 在看官方推薦的做法前,先來看一下幾個常見的老方法。 以下討論,讓我們先假設 App 的進入點為專案預設的 MainActivity。 2-1 在 MainActivity 前新增一個 SplashScreenActivity 新增一個 SplashScreenActivity。 在 SplashScreenActivity 的 UI 中繪製歡迎畫面。 使用 Thread.sleep() 設定等待秒數。 執行完後跳轉到 MainActivity。 簡單、直覺的一個方法。 設計師要一個歡迎畫面,我就刻一個出來。 搭配 Thread.sleep(),要顯示幾秒就顯示幾秒。 不過,在 SplashScreenActivity 顯示前,使用者還是會看到系統預設的歡迎畫面。 實際進入 App 的使用體驗可能會跟設計師預期的不同,而且有可能等待時間比預期的還要久。 2-2 SplashScreenActivity 搭配 windowBackground 新增一個 SplashScreenActivity。 在 /drawable 中新增一個 layer-list 的 splash_backgroun.xml 1 2 3 4 5 6 7 <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/white" /> <item android:drawable="@drawable/ic_icon_vector" android:gravity="center"/> </layer-list> 在 themes.xml 中新增 SplashScreenTheme。 1 2 3 4 <!-- Splash Screen theme. --> <style name="SplashScreenTheme" parent="Theme.AppCompat.NoActionBar"> <item name="android:windowBackground">@drawable/splash_background</item> </style> 設定 SplashScreenActivity 的 Theme。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="yourpackage"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".SplashScreenActivity" android:theme="@style/SplashScreenTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".MainActivity" /> </application> </manifest> SplashScreenActivity 執行完後跳轉到 MainActivity。 1 2 3 4 5 6 7 8 public class SplashScreenActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); startActivity(new Intent(SplashScreenActivity.this, MainActivity.class)); finish(); } 跟前一個方法很類似,不過因為是使用 windowBackground 的設定,使用者不會看到系統的歡迎畫面。 ...

Published on January 27, 2021 · 4 min · 783 words · Daniel Huang