2020年11月19日木曜日

MaterialAlertDialogBuilder のボタンの色を変更する

non-Bridge なテーマで MaterialAlertDialogBuilder を使うと、ダイアログのボタンの色は colorPrimary になります。

そのため colorPrimary に黒っぽい色を指定した DayNight テーマだと、Dark Mode のときにボタンの文字が見えないという状態になってしまいます。
  1. <resources>  
  2.   
  3.     <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">  
  4.         <item name="colorPrimary">#212121</item>  
  5.     </style>  
  6.   
  7. </resources>  




DayNight テーマの colorPrimary は変えずに Dark Mode のときだけダイアログのボタンの色を変えるには、materialAlertDialogTheme 属性を指定します。

res/values/themes.xml
  1. <resources>  
  2.   
  3.     <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">  
  4.         <item name="colorPrimary">#212121</item>  
  5.   
  6.         <item name="materialAlertDialogTheme">@style/ThemeOverlay.MyApp.MaterialAlertDialog</item>  
  7.     </style>  
  8.   
  9.     <style name="ThemeOverlay.MyApp.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog" />  
  10.   
  11. </resources>  
res/values-night-v8/themes.xml
  1. <resources>  
  2.   
  3.     <style name="ThemeOverlay.MyApp.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">  
  4.         <item name="colorPrimary">#ffffff</item>  
  5.     </style>  
  6.   
  7. </resources>  




2020年11月12日木曜日

複数の nullable な mutable 変数の null チェック

nullable な mutable 変数 a と b があって、両方 non-null のとき何かしたいという場合、

これだとコンパイルエラーになる
  1. var a: String? = "Hello"  
  2. var b: String? = "Android"  
  3.   
  4. fun main() {  
  5.     if (a != null && b != null) {  
  6.         println(a.length + b.length) // compile error!  
  7.     }  
  8. }  
let だと入れ子になるからいまいち
  1. var a: String? = "Hello"  
  2. var b: String? = "Android"  
  3.   
  4. fun main() {  
  5.     a?.let { a ->  
  6.         b?.let { b ->  
  7.             println(a.length + b.length)  
  8.         }  
  9.     }  
  10. }  
ローカルの変数に入れれば OK
  1. var a: String? = "Hello"  
  2. var b: String? = "Android"  
  3.   
  4. fun main() {  
  5.     val a = a  
  6.     val b = b  
  7.     if (a != null && b != null) {  
  8.         println(a.length + b.length) // ok  
  9.     }  
  10. }  
vararg を使って、渡した値が全部 non-null だったら block を実行するというメソッドを用意すると、let っぽい感じで入れ子にせずに書ける
  1. var a: String? = "Hello"  
  2. var b: String? = "Android"  
  3.   
  4. fun main() {  
  5.     doIfAllNotNull(a, b) { (a, b) ->  
  6.         println(a.length + b.length)  
  7.     }  
  8. }  
  9.   
  10. private inline fun <T> doIfAllNotNull(  
  11.     vararg value: T?,  
  12.     block: (values: List<T>) -> Unit  
  13. ) {  
  14.     val nonNullValues = value.filterNotNull()  
  15.     if (nonNullValues.size == value.size) {  
  16.         block(nonNullValues)  
  17.     }  
  18. }  

2020年11月11日水曜日

Kotlin メモ : vetoable

vetoable

変更を拒否(veto)するかどうかのコールバックを指定できる property delegate を返す。

  1. fun main()  {  
  2.     var value: Int by Delegates.vetoable(0) { property, oldValue, newValue ->  
  3.         newValue > 0  
  4.     }  
  5.   
  6.     println(value) // 0  
  7.   
  8.     value = 10  
  9.     println(value) // 10  
  10.   
  11.     value = -1  
  12.     println(value) // 10  
  13. }  

2020年11月10日火曜日

ConstraintSet を使って Activity を再生成せずにレイアウトを切り替える

自分で縦横レイアウトを切り替えるので Manifest に設定を追加
  1. <activity  
  2.     android:name=".MainActivity"  
  3.     android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">  
  4.   
  5.     ...  
  6.   
  7. </activity>  
縦画面用のレイアウトと、横画面用のレイアウトを用意する。
例としてここでは縦画面では上部に 16:9 で配置し、横画面では全画面に配置している。

縦画面用
res/layout/activity_main_port.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"  
  4.     xmlns:tools="http://schemas.android.com/tools"  
  5.     android:id="@+id/constraintLayout"  
  6.     android:layout_width="match_parent"  
  7.     android:layout_height="match_parent"  
  8.     tools:context=".MainActivity">  
  9.   
  10.     <include  
  11.         android:id="@+id/container"  
  12.         layout="@layout/activity_main"  
  13.         android:layout_width="0dp"  
  14.         android:layout_height="0dp"  
  15.         app:layout_constraintDimensionRatio="16:9"  
  16.         app:layout_constraintEnd_toEndOf="parent"  
  17.         app:layout_constraintStart_toStartOf="parent"  
  18.         app:layout_constraintTop_toTopOf="parent" />  
  19.   
  20. </androidx.constraintlayout.widget.ConstraintLayout>  
横画面用
res/layout/activity_main_land.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"  
  4.     xmlns:tools="http://schemas.android.com/tools"  
  5.     android:id="@+id/constraintLayout"  
  6.     android:layout_width="match_parent"  
  7.     android:layout_height="match_parent"  
  8.     tools:context=".MainActivity">  
  9.   
  10.     <include  
  11.         android:id="@+id/container"  
  12.         layout="@layout/activity_main"  
  13.         android:layout_width="0dp"  
  14.         android:layout_height="0dp"  
  15.         app:layout_constraintBottom_toBottomOf="parent"  
  16.         app:layout_constraintEnd_toEndOf="parent"  
  17.         app:layout_constraintStart_toStartOf="parent"  
  18.         app:layout_constraintTop_toTopOf="parent" />  
  19.   
  20. </androidx.constraintlayout.widget.ConstraintLayout>  
activity_main_port.xml と activity_main_land.xml からそれぞれ ConstraintSet を構成する。
onConfigurationChanged() で画面の向きに応じて対応する ConstraintSet を ConstraintLayout に applyTo() する。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     private val constraintSetPort = ConstraintSet()  
  4.     private val constraintSetLand = ConstraintSet()  
  5.   
  6.     private lateinit var constraintLayout: ConstraintLayout  
  7.   
  8.     override fun onCreate(savedInstanceState: Bundle?) {  
  9.         super.onCreate(savedInstanceState)  
  10.         val layoutId = when (resources.configuration.orientation) {  
  11.             Configuration.ORIENTATION_LANDSCAPE -> R.layout.activity_main_land  
  12.             else -> R.layout.activity_main_port  
  13.         }  
  14.         setContentView(layoutId)  
  15.         constraintLayout = findViewById(R.id.constraintLayout)  
  16.   
  17.         constraintSetPort.clone(this, R.layout.activity_main_port)  
  18.         constraintSetLand.clone(this, R.layout.activity_main_land)  
  19.     }  
  20.   
  21.     override fun onConfigurationChanged(newConfig: Configuration) {  
  22.         super.onConfigurationChanged(newConfig)  
  23.   
  24.         val constraintSet = when (newConfig.orientation) {  
  25.             Configuration.ORIENTATION_LANDSCAPE -> constraintSetLand  
  26.             else -> constraintSetPort  
  27.         }  
  28.         constraintSet.applyTo(constraintLayout)  
  29.     }  
  30. }  






2020年11月5日木曜日

Android 11 では ACTION_CREATE_DOCUMENT と ACTION_OPEN_DOCUMENT には <queries> 指定が必要

Intent.ACTION_CREATE_DOCUMENT および Intent.ACTION_OPEN_DOCUMENT を startActivityForResult() で呼び出す以下のコードは、targetSdkVersion が 29 (Android 10)までは問題なく動きます。
  1.  private fun createDocument() {  
  2.     val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)  
  3.         .addCategory(Intent.CATEGORY_OPENABLE)  
  4.         .setType("*/*")  
  5.         .putExtra(Intent.EXTRA_TITLE, "log.txt")  
  6.   
  7.     if (intent.resolveActivity(packageManager) != null) {  
  8.         startActivityForResult(intent, REQUEST_CODE_CREATE_DOCUMENT)  
  9.     } else {  
  10.         Toast.makeText(  
  11.             this,  
  12.             "Unable to resolve Intent.ACTION_CREATE_DOCUMENT",  
  13.             Toast.LENGTH_SHORT  
  14.         )  
  15.             .show()  
  16.     }  
  17. }  
  18.   
  19. private fun openDocument() {  
  20.     val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)  
  21.         .addCategory(Intent.CATEGORY_OPENABLE)  
  22.         .setType("*/*")  
  23.   
  24.     if (intent.resolveActivity(packageManager) != null) {  
  25.         startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT)  
  26.     } else {  
  27.         Toast.makeText(  
  28.             this,  
  29.             "Unable to resolve Intent.ACTION_OPEN_DOCUMENT",  
  30.             Toast.LENGTH_SHORT  
  31.         )  
  32.             .show()  
  33.     }  
  34. }  
しかし、targetSdkVersion を 30 (Android 11)にすると、intent.resolveActivity(packageManager) が null を返すようになってしまいます。

Android 11 では AndroidManifest.xml に以下の <queries> を指定すると動くようになります。
  1. <manifest ...>  
  2.   
  3.     ...  
  4.   
  5.     <queries>  
  6.         <intent>  
  7.             <action android:name="android.intent.action.CREATE_DOCUMENT" />  
  8.             <data android:mimeType="*/*" />  
  9.         </intent>  
  10.         <intent>  
  11.             <action android:name="android.intent.action.OPEN_DOCUMENT" />  
  12.             <data android:mimeType="*/*" />  
  13.         </intent>  
  14.     </queries>  
  15.   
  16.     ...  
  17. </manifest>