2024-04-07

【Kotlin】以資源檔自定向量圖 ImageButton(View) 外觀

參考資料 ----


覺得以程式控制 ImageButton 的各種狀態(按下按鈕、disabled...) 時的外觀很麻煩,可以透過設定資源檔的方式達到目的。
由於 ImageButton 繼承自 View,原則上,本筆記的做法可以套用 View(含) 的其他子類別。
 

app 層級 build.gradle(若您的程式沒有用到視圖綁定,這部份可忽略)
 
...
...
 
android {
    
    ...
    ...
    
    buildFeatures {
        viewBinding true
    }
    
    ...
    ...
 


色表 /values/color.xml
 
<?xml version="1.0" encoding="utf-8"?>
<resources>

    ...
    ...
	
    <color name="blue">#0000FF</color>
    <color name="red">#ffff0000</color>
    <color name="brown">#8B4513</color>
    <color name="gray">#ff888888</color>
    <color name="green">#ff00ff00</color>
    <color name="yellow">#FFFF00</color>
	
    ...
    ...
</resources>
 


分別建立 前景(向量圖)背景 在各種狀態時的顏色的資源檔,並置於 /res/color/
要注意:Android 套用資源檔的原則是以最低限度符合條件就採用,所以預設值要放在最後順位

前景 /res/color/image_state.xml,預設一般狀態是 藍色,按下按鈕時是 棕色,disabled 時是 灰色
 
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true"
        android:color="@color/brown" />
    <item android:state_enabled="false"
        android:color="@color/gray" />
    <item android:color="@color/blue" />    ← 預設值要放在最後順位
</selector>
 


背景 /res/color/back_state.xml,無預設值(所以跟 app 的 Theme 設定相同),按下按鈕時是 黃色,disabled 時是 綠色
 
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true"
        android:color="@color/yellow" />
    <item android:state_enabled="false"
        android:color="@color/green" />
</selector> 


隨手找了個向量圖檔 /res/drawable/hand.xml,tint 屬性指向 image_state.xml
(tint 有著色的意思)
 
<vector
    android:height="48dp"
    android:width="48dp"
    android:tint="@color/image_state"    ← 這裡指向 前景 image_state.xml
    android:viewportHeight="24"
    android:viewportWidth="24"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <path
        android:fillColor="@android:color/white"
        android:pathData="M13,24c-3.26,0 -6.19,-1.99 -7.4,-5.02l-3.03,-7.61C2.26,10.58 3,9.79 3.81,10.05l0.79,0.26c0.56,0.18 1.02,0.61 1.24,1.16L7.25,15H8V3.25C8,2.56 8.56,2 9.25,2s1.25,0.56 1.25,1.25V12h1V1.25C11.5,0.56 12.06,0 12.75,0S14,0.56 14,1.25V12h1V2.75c0,-0.69 0.56,-1.25 1.25,-1.25c0.69,0 1.25,0.56 1.25,1.25V12h1V5.75c0,-0.69 0.56,-1.25 1.25,-1.25S21,5.06 21,5.75V16C21,20.42 17.42,24 13,24z"/>
</vector>
 


activity_main.xml,ImageButton 的 backgroundTint 屬性指向 back_state.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">

    <Button
        android:id="@+id/Button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:onClick="onButton1Clicked"/>

    <ImageButton
        android:id="@+id/Button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        app:srcCompat="@drawable/hand"
        android:backgroundTint="@color/back_state"    ← 這裡指向 背景 back_state.xml
        android:clickable="true" />

</RelativeLayout>
 


MainActivity.kt (程式沒什麼功能,只是觀察 Button2enabled / disabled 時的變化)
 

...
...

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }

    fun onButton1Clicked(vv: View) {
        binding.Button2.isEnabled = !binding.Button2.isEnabled
    }
}
 


相關筆記 ----


2024-04-06

【Kotlin】刪除指定目錄內的所有檔案

 
class MainActivity : AppCompatActivity() {

    fun onButtonClicked(vv: View) {
        // 刪除內部儲存空間的所有檔案
        delAll(filesDir.toString())
    }

    ...
    ...
    
    fun delAll(sDir: String) {
        val mScope = CoroutineScope(Job() + Dispatchers.IO)
        mScope.launch {
            val directory = File(sDir)
            val files = directory.listFiles()
            for (file in files) {
                file.delete()
            }
        }
    }
}
 


【Kotlin】查詢 Android 可用外部儲存空間

參考資料 ----


Android app,可儲存 app 自身專屬(用) 檔案於 內部儲存空間外部儲存空間,內部儲存空間很小,只適合存放小容量的檔案,若不是有特殊需求,官方建議存於外部儲存空間。

雖然是使用外部儲存空間,但因為僅供 app 自身專用,所以並不須向系統請求權限。

 
class MainActivity : AppCompatActivity() {
    private val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
  
        // 查詢磁碟區狀態
        val sExternalStorage = Environment.getExternalStorageState()
        Log.d(TAG, "可用外部空間媒體資訊: $sExternalStorage")    // mounted: 可讀/寫
        // 狀態有: MEDIA_UNKNOWN, MEDIA_REMOVED, MEDIA_UNMOUNTED, MEDIA_CHECKING, MEDIA_NOFS, MEDIA_MOUNTED, MEDIA_MOUNTED_READ_ONLY, MEDIA_SHARED, MEDIA_BAD_REMOVAL, MEDIA_UNMOUNTABLE
  
        // 以前的 Android 因為 ROM 小, 多半會有 SD 卡插槽
        // 現在 ROM 變大了, 也漸漸不提供 SD 卡插槽了, 並模擬出一塊虛擬 ROM
        val externalStorageVolumes: Array<out File> = ContextCompat.getExternalFilesDirs(applicationContext, null)
        Log.d(TAG, "可用外部空間媒體: ${externalStorageVolumes.size} 個")    // 2 個, 1 個實體 SD 卡, 1 個虛擬 ROM
  
        for(xx in externalStorageVolumes.indices) {
            val primaryExternalStorage = externalStorageVolumes[xx]
            Log.d(TAG, "可用外部空間 $xx: $primaryExternalStorage")
            // 可用外部空間 0: /storage/emulated/0/Android/data/[您的 app 完整 package 名]/files, 虛擬 ROM 通常會是第 1 個
            // 可用外部空間 1: /storage/9E1E-5F1E/Android/data/[您的 app 完整 package 名]/files, 實體 SD 卡
        }
  
        if(Environment.getExternalStorageState()==Environment.MEDIA_MOUNTED) {
            // 有外部儲存空間媒體, 且可 讀/寫
            // 官方強烈建議, 若沒有特殊需要, 優先使用第 1 個可用外部空間
            val mFilePath = externalStorageVolumes[0]
            Log.d(TAG, "mFilePath: $mFilePath")    //   /storage/emulated/0/Android/data/[您的 app 完整 package 名]/files
            val mExtdir = File(getExternalFilesDir(null), "Chkspace")

            // 查詢可用空間
            val mScope = CoroutineScope(Job() + Dispatchers.IO)
            mScope.launch {
                var mBytes: Long = 0
                // 1. 由於在 Android8(Oreo) 推出新的 API26,
                // storageManager.getUuidForPath 不能在主執行緒呼叫, 因此要在 coroutine 內執行
                // 2. 舊的 API 在未來會被棄用, 所以要分開處理
                if(Build.VERSION.SDK_INT>=26) {
                    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
                    val uuid = storageManager.getUuidForPath(externalStorageVolumes[0])
                    try {
                        mBytes = storageManager.getAllocatableBytes(uuid)
                    } catch(err: IOException) {
                        err.printStackTrace()
                    }
                } else {
                    val path = Environment.getDataDirectory()
                    val stat = StatFs(path.path)
                    val blockSize: Long = stat.blockSizeLong
                    val availableBlocks: Long = stat.availableBlocksLong
                    mBytes = availableBlocks * blockSize
                }
                val formatter = DecimalFormat("#,###")
                Log.d(TAG, "可用剩餘外部儲存空間 = ${formatter.format(mBytes)} ")

                if(mBytes>=1024) {
                    val mKB = (mBytes/1024) // 換算為 kb
                    if(mKB>=1024) {
                        val mmMB = (mKB/1024) // 換算為 mb
                        if(mmMB>=1024) {
                            val mGB = (mmMB/1024) // 換算為 gb
                            Log.d(TAG, "可用剩餘外部儲存空間 = $mGB gb")
                        } else
                            Log.d(TAG, "可用剩餘外部儲存空間 = $mmMB mb")
                    } else {
                        Log.d(TAG, "可用剩餘外部儲存空間 = $mKB kb")
                    }
                } else {
                    Log.d(TAG, "可用剩餘外部儲存空間 = $mBytes bytes")
                }
            }
        }
    }
}
 


【Kotlin】Coroutine 複製 assets 的檔案到內部儲存空間

參考資料 ----

 
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

	// 檢查內部儲存空間是否已存在檔案
    val file = File(this.filesDir, "mymusic.mp3")
    if(!file.exists()) {
            copyAssets()
    } else {
        Toast.makeText(this, "mymusic.mp3 已複製到私有儲存空間", Toast.LENGTH_LONG).show();
    }
}

...
...

// 複製 mymusic.mp3 至私有儲存空間
private fun copyAssets() {
    val mScope = CoroutineScope(Job() + Dispatchers.IO)
    mScope.launch {
        try {
            val outfile = FileOutputStream(File(filesDir, "mymusic.mp3").path)
            val infile: InputStream = assets.open("mymusic.mp3")
            infile.copyTo(outfile)
            outfile.close()
            infile.close()
        } catch(err: IOException) {
            err.printStackTrace();
        }
    }
}
 


相關筆記 ----


2024-02-26

【Kotlin】disable/enable ImageButton

參考資料 ----


雖然在 layout.xml 中定義 enabled="false"AndroidStudio 並不會報錯,但在 app 執行時並沒有產生作用;官網的 ImageButton 參考頁也沒有找到 enabled 的屬性,似乎只能在 app 執行時設定。
 
...
...

<ImageButton
    android:id="@+id/Button1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/圖檔"    本次採用 向量圖檔(vector asset), 所以可在程式中改變圖檔的顏色
    android:background="@android:color/transparent"    指定按鈕的背景為透明
    android:enabled="true" />    這個屬性在 app 執行時並沒有產生作用
 


 
 
... 
...

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var button1 = findViewById<ImageButton>(R.id.Button1)
        button1.isEnabled = false
        // 即使被 disabled 了, button1 所引用的圖檔顏色並沒有任何變化
        // 所以也要自行改變圖檔的顏色
        button1.setColorFilter(getColor(R.color.black))
    }
 


2024-02-13

【MSSQLserver】日期的運算(適 MSSQL2016(含)後版本)

參考資料 ----
DATEPART (Transact-SQL)

 
-- 假設給予日期:2024-2-2(五)


-- 求得 2024-2-2 是當年的第幾週
SELECT DATEPART(week, '2024-2-2')    -- 第 5 週


-- 求得 2024-2-2 是星期幾
SELECT DATEPART(weekday, '2024-2-2')    -- 星期五


-- 當月首日:2024-2-1
-- 方法一
-- EOMONTH 求得上個月的最末日
-- DATEADD 再運算加 1 日
SELECT DATEADD(day, 1, EOMONTH('2024-2-2',-1))

-- 方法二
SELECT DATEFROMPARTS(YEAR('2024-2-2'),MONTH('2024-2-2'),1)


-- 當月最末日:2024-2-29
SELECT EOMONTH('2024-2-2')


-- 當年首日:2024-1-1
SELECT DATEFROMPARTS(YEAR('2024-2-2'),1,1)


-- 當年最末日:2024-12-31
SELECT DATEFROMPARTS(YEAR('2024-2-2'), 12, 31)
 


2024-02-07

【Kotlin】設為 深色/夜晚 模式

參考資料 ----
Android深色模式适配原理分析(這篇解說超清楚的)


踩到雷!
下面程式碼在陽春的 app 可行,但 強制深色模式會令 Activity 重建一個 instance(實例),以 onCreate() 為例,會同時間執行 2 次;這次的雷就是 SQLite 同時間寫入 2 筆相同的記錄,造成 primary key 衝突才發現。

Android Studio 精靈建立新專案,若看到有 /res/values-night/ 目錄,且目錄下有 themes.xml,就表示精靈有幫您建立 深色/夜晚 模式的主題配置。
 
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        ...
        ...

        // 設為 深色/夜晚 模式
        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)

        ...
        ...
}
 
這指令到底該在什麼場合使用...再說吧... 😝


目前找到的替代做法,是修改預設(白天) 的 themes.xml
 
<style name="Theme.blablabla" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
	...
	...
	
    <!-- Customize your theme here. -->
    <item name="android:windowBackground">@color/black</item>
 

2024-02-01

【LinuxMint20.3】以 docker 安裝 Apache Superset -- 免費,開源的 BI

參考資料 ----


Superset 的安裝方式有:
  • Linux 套件安裝
  • 以 Docker Compose 佈署
  • 以 Kubernetes(K8S) 佈署
  • 在 Dockerhub 執行
  • 以 Pypi 下載安裝
  • 自 GitHub 下載安裝
  • 自 Apache 基金會官網下載安裝


其中 Docker Compose 只支援單機運行且不支援高可用性(HA),因此官方不建議安裝使用於正式生產環境(production);聽起來,似乎雖然不是開發版本,但適合快速安裝嚐鮮用。本筆記採用 Docker Compose 佈署


安裝必要的相依套件
 
[user]$ sudo  apt  install  git  docker  docker-compose
 


複製資源庫
 
# 預定裝在 /usr/share
[user]$ cd  /usr/share
[user]$ sudo  git  clone  https://github.com/apache/superset.git

# 會看到生成 /usr/share/superset/ 目錄
 


執行 docker daemon
 
[user]$ cd  superset
# 執行 docker daemon
[user]$ sudo  systemctl  start  docker

# 檢查確認 docker daemon 執行狀態
[user]$ sudo  systemctl  status  docker
 


啟動 superset
 
# 第一次執行, 下傳 container, 一次性指令
[user]$ sudo  docker-compose  -f  docker-compose-non-dev.yml  pull

# 之後只要執行這指令
[user]$ sudo  docker-compose  -f  docker-compose-non-dev.yml  up
 


若要停止執行 superset,則 CTRL + C
這時會看到螢幕顯示
Stopping superset_app...
Stopping superset_worker_beat...
...
等正在停止程序的訊息

superset 內建並不支援 Microsoft SQL Server,若要連接 mssql,則要安裝驅動程式,官方建議的驅動為 pymssql
 
[user]$ sudo  vim  /usr/share/superset/docker/requirements-local.txt
# 輸入
pymssql

:x 存檔離開

# 再執行
[user]$ sudo  docker-compose  -f  docker-compose-non-dev.yml  up
# 就會自動安裝 pymssql 了
 


開啟瀏覽器,輸入網址
http://localhost:8088
登入帳/密:admin / admin

建立資料庫連線

點擊右上角的 "Settings"
Database Connections



+DATABASE
→ 可以看到列出的預設支援資料庫沒有 MS SQL Server
→ 下拉下方的 Supported databases,選擇最末的 Other



DISPLAY NAME (必填) 命名您的資料庫連線
SQLALCHEMY URI(必填) 輸入連線字串,格式為
 
mssql+pymssql://username:password@hostname:port/database_name

* 若 password 內含有 @ 字元,要改為 URL編碼(percent-encoding),改成 %40
* port 的部份,若您的 mssql 採用預設的 port,則可不填
 
→ 點擊 TEST CONNECTION 鈕確認連線正常
→ CONNECT 建立連線





設定為開機自動執行 superset

 
[user]$ sudo  vim  /etc/systemd/system/superset.service
# 輸入下述指令
[Unit]
Description=Apache  Superset
After=docker.service

[Service]
Type=simple
ExecStart=/usr/bin/docker-compose  -f  /usr/share/superset/docker-compose-non-dev.yml  up
ExecStop=/usr/bin/docker-compose  -f  /usr/share/superset/docker-compose-non-dev.yml  down

[Install]
WantedBy=multi-user.target

:x 存檔離開

# 啟用 docker.service 開機自動執行
[user]$ sudo  systemctl  enable  docker.service

# 啟用 superset.service 開機自動執行
[user]$ sudo  systemctl  enable  superset.service
 
重新開機,驗證 superset 確實開機自動執行

版本號碼為 0.0.0-dev,在 Apache Superset 0.0.0dev Overview 有解釋。


2024-01-31

【Metabase】設定 Metabase 為開機即啟動服務

參考資料 ----

metabase.jar 移至 /usr/share/metabase/ 目錄


 
[user]$ sudo  su
[root]# groupadd  -r  metabase
[root]# useradd  -r  -s  /bin/false  -g  metabase  metabase
[root]# chown  -R  metabase:metabase  /usr/share/metabase
[root]# touch  /var/log/metabase.log
[root]# chown  syslog:adm  /var/log/metabase.log
[root]# touch  /etc/default/metabase
[root]# chmod  640  /etc/default/metabase
 


建立 Metabase Service 服務檔
 
[root]# cd  /etc/systemd/system/
[root]# vim metabase.service
# 輸入下述內容
[Unit]
Description=Metabase server
After=syslog.target
After=network.target

[Service]
WorkingDirectory=/usr/share/metabase/
ExecStart=/usr/bin/java  -jar  /usr/share/metabase/metabase.jar
EnvironmentFile=/etc/default/metabase
User=metabase
Type=simple
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=metabase
SuccessExitStatus=143
TimeoutStopSec=120
Restart=always

[Install]
WantedBy=multi-user.target

:x 存檔離開
 


建立 syslog conf
 
[root]# vim  /etc/rsyslog.d/metabase.conf
# 輸入下述內容
if $programname == 'metabase' then /var/log/metabase.log
& stop

:x 存檔離開

# 重啟 rsyslog.service 以使 metabase.conf 生效
[root]# systemctl  restart  rsyslog.service
 


編輯 Metabase 的環境變數
 
[root]# vim  /etc/default/metabase
MB_PASSWORD_COMPLEXITY=strong
MB_PASSWORD_LENGTH=密碼字串長度
MB_JETTY_HOST=0.0.0.0
MB_JETTY_PORT=3000
MB_DB_TYPE=mysql
MB_DB_DBNAME=metabase
MB_DB_PORT=3306
MB_DB_USER=metabase
MB_DB_PASS=MySQL的metabase帳戶密碼
MB_DB_HOST=localhost
MB_EMOJI_IN_LOGS=true    # log 要不要有顏文字...這還蠻有趣的...若不要就填 false
# any other env vars you want available to Metabase
 


設定 metabase 為開機啟動
 
# 先手動啟動 metabase.service
[root]# systemctl  daemon-reload
[root]# systemctl  start  metabase.service
[root]# systemctl  status  metabase.service

# 確定執行沒問題了, 就改為開機自動啟動
[root]# systemctl  enable  metabase.service
 



相關筆記 ----

2024-01-29

【Metabase】社群版(Community) 將系統資料移至大型資料庫

參考資料 ----


Metabase 自帶的系統資料庫為 H2,僅用於測試試用期間,若要轉為正式應用(production),官方強烈將系統資料移至大型資料庫。

建議版本為 MySQL 5.7.7(含)MariaDB 10.2.2(含) 或 PostgreSQL 9.4(含) 以上;本筆記為 MySQL 8.0

注意:避免同時做 Metabase 更新升級 及 資料庫移植


關閉 Metabase,目前我的練功主機是開終端機視窗,在家目錄以
 
[user]~$ java  -jar  metabase.jar
 
的指令啟動 Metabase
所以只要在瀏覽器的 Metabase 頁面登出,然後在終端機視窗 Ctrl+C 中斷 Metabase 即可。


備份 H2︰複製 metabase.db.mv.db 到另一個安全的目錄存放。


操作 phpMyAdmin
MySQL 新增 metabase 帳戶


建立 metabase 空白資料庫...我其實對資料庫的字集沒有概念,所以隨便選。


授予 metabase 帳戶對 metabase 資料庫具有全部的權限










執行移植指令
 
# 切換到 metabase.jar 所在目錄, 在本例為 [user]~/metabase/
[user]~/metabase$  export MB_DB_TYPE=mysql
[user]~/metabase$  export MB_DB_CONNECTION_URI="jdbc:mysql://主機IP:3306/metabase?user=metabase帳戶&password=密碼"
[user]~/metabase$  java  -DMB_DB_TYPE=mysql  -DMB_DB_CONNECTION_URI="jdbc:mysql://主機IP:3306/metabase?user=帳號&password=密碼;"  -jar  metabase.jar  load-from-h2  metabase.db    # 注意檔名
 


因為有備份 H2,所以可以放心刪除 metabase 所在目錄下的 H2 檔案,以確認 metabase 存取的是 MySQL。

重新啟動 metabase
 
[user]~/metabase$  export MB_DB_TYPE=mysql
[user]~/metabase$  export MB_DB_CONNECTION_URI="jdbc:mysql://主機IP:3306/metabase?user=metabase帳戶&password=密碼"
[user]~/metabase$  java  -DMB_DB_TYPE=mysql  -DMB_DB_CONNECTION_URI="jdbc:mysql://主機IP:3306/metabase?user=metabase帳號&password=密碼"  -jar  metabase.jar
 
重登入 metabase,可以看到之前製作的儀表板都在。


相關筆記 ----