2020年3月18日水曜日

Kotlin メモ : repeat

repeat

指定回数だけ action を実行します。
  1. val count = supportFragmentManager.backStackEntryCount  
  2. repeat(count) {  
  3.     supportFragmentManager.popBackStack()  
  4. }  




2020年3月16日月曜日

Drag を実装する その2 : GestureDetector

GestureDetector を使うと onScroll() で移動距離を教えてくれる。ただし、GestureDetector は ACTION_UP や ACTION_CANCEL を通知してくれないのが難点である。
  1. class SimpleDragView : FrameLayout {  
  2.   
  3.     constructor(context: Context) : super(context)  
  4.     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)  
  5.     constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(  
  6.         context,  
  7.         attrs,  
  8.         defStyleAttr  
  9.     )  
  10.   
  11.     private val targetView: View  
  12.   
  13.     private val gestureDetector: GestureDetector  
  14.   
  15.     init {  
  16.         val size = (100 * resources.displayMetrics.density).toInt()  
  17.         targetView = View(context).apply {  
  18.             layoutParams = LayoutParams(size, size)  
  19.             setBackgroundColor(Color.RED)  
  20.         }  
  21.         addView(targetView)  
  22.   
  23.         gestureDetector =  
  24.             GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {  
  25.                 override fun onScroll(  
  26.                     e1: MotionEvent,  
  27.                     e2: MotionEvent,  
  28.                     distanceX: Float,  
  29.                     distanceY: Float  
  30.                 ): Boolean {  
  31.   
  32.                     targetView.translationX -= distanceX  
  33.                     targetView.translationY -= distanceY  
  34.   
  35.                     return true  
  36.                 }  
  37.   
  38.                 override fun onDown(e: MotionEvent): Boolean {  
  39.   
  40.                     val pointerIndex = e.actionIndex  
  41.                     val x = e.getX(pointerIndex)  
  42.                     val y = e.getY(pointerIndex)  
  43.   
  44.                     if (!(x.toInt() in 0..width && y.toInt() in 0..height)) {  
  45.                         return false  
  46.                     }  
  47.   
  48.                     val left = targetView.translationX  
  49.                     val right = left + targetView.width  
  50.                     val top = targetView.translationY  
  51.                     val bottom = top + targetView.height  
  52.   
  53.                     if (!(x in left..right && y in top..bottom)) {  
  54.                         return false  
  55.                     }  
  56.   
  57.                     return true  
  58.                 }  
  59.             })  
  60.         gestureDetector.setIsLongpressEnabled(false)  
  61.     }  
  62.   
  63.     override fun onTouchEvent(ev: MotionEvent): Boolean {  
  64.         return gestureDetector.onTouchEvent(ev) || super.onTouchEvent(ev)  
  65.     }  
  66. }  



setIsLongpressEnabled(false) しないと下のように LongPress 判定されたときに onScroll() が呼ばれない。




assets 内のファイルの url

いつも忘れるのでメモっておく

src/main/assets/index.html の url は
  1. file:///android_asset/index.html  



2020年3月13日金曜日

Drag を実装する その1 : GestureDetector なし

Drag and scale | Android Developers (MotionEventCompat を使ってたりちょっと古い)を参考に変えたもの
  1. class SimpleDragView : FrameLayout {  
  2.   
  3.     constructor(context: Context) : super(context)  
  4.     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)  
  5.     constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(  
  6.         context,  
  7.         attrs,  
  8.         defStyleAttr  
  9.     )  
  10.   
  11.     private var activePointerId = INVALID_POINTER_ID  
  12.     private var lastTouchX = 0f  
  13.     private var lastTouchY = 0f  
  14.   
  15.     private val targetView: View  
  16.   
  17.     init {  
  18.         val size = (100 * resources.displayMetrics.density).toInt()  
  19.         targetView = View(context).apply {  
  20.             layoutParams = LayoutParams(size, size)  
  21.             setBackgroundColor(Color.RED)  
  22.         }  
  23.         addView(targetView)  
  24.     }  
  25.   
  26.     override fun onTouchEvent(ev: MotionEvent): Boolean {  
  27.         when (ev.actionMasked) {  
  28.             MotionEvent.ACTION_DOWN -> {  
  29.                 val pointerIndex = ev.actionIndex  
  30.                 val x = ev.getX(pointerIndex)  
  31.                 val y = ev.getY(pointerIndex)  
  32.   
  33.                 if (!(x.toInt() in 0..width && y.toInt() in 0..height)) {  
  34.                     return false  
  35.                 }  
  36.   
  37.                 val left = targetView.translationX  
  38.                 val right = left + targetView.width  
  39.                 val top = targetView.translationY  
  40.                 val bottom = top + targetView.height  
  41.   
  42.                 if (!(x in left..right && y in top..bottom)) {  
  43.                     return false  
  44.                 }  
  45.   
  46.                 lastTouchX = x  
  47.                 lastTouchY = y  
  48.   
  49.                 activePointerId = ev.getPointerId(0)  
  50.             }  
  51.             MotionEvent.ACTION_MOVE -> {  
  52.                 if (activePointerId == INVALID_POINTER_ID) {  
  53.                     return false  
  54.                 }  
  55.   
  56.                 val pointerIndex = ev.findPointerIndex(activePointerId)  
  57.                 val x = ev.getX(pointerIndex)  
  58.                 val y = ev.getY(pointerIndex)  
  59.   
  60.                 if (!(x.toInt() in 0..width && y.toInt() in 0..height)) {  
  61.                     return false  
  62.                 }  
  63.   
  64.                 val diffX = x - lastTouchX  
  65.                 val diffY = y - lastTouchY  
  66.   
  67.                 targetView.translationX += diffX  
  68.                 targetView.translationY += diffY  
  69.   
  70.                 lastTouchX = x  
  71.                 lastTouchY = y  
  72.             }  
  73.             MotionEvent.ACTION_UP,  
  74.             MotionEvent.ACTION_CANCEL -> {  
  75.                 activePointerId = INVALID_POINTER_ID  
  76.             }  
  77.             MotionEvent.ACTION_POINTER_UP -> {  
  78.                 if (activePointerId == INVALID_POINTER_ID) {  
  79.                     return false  
  80.                 }  
  81.   
  82.                 val pointerIndex = ev.actionIndex  
  83.                 if (ev.getPointerId(pointerIndex) != activePointerId) {  
  84.                     return false  
  85.                 }  
  86.   
  87.                 val newPointerIndex = if (pointerIndex == 01 else 0  
  88.                 val x = ev.getX(newPointerIndex)  
  89.                 val y = ev.getY(newPointerIndex)  
  90.   
  91.                 if (!(x.toInt() in 0..width && y.toInt() in 0..height)) {  
  92.                     activePointerId = INVALID_POINTER_ID  
  93.                     return false  
  94.                 }  
  95.   
  96.                 lastTouchX = x  
  97.                 lastTouchY = y  
  98.   
  99.                 activePointerId = ev.getPointerId(newPointerIndex)  
  100.             }  
  101.         }  
  102.   
  103.         return true  
  104.     }  
  105. }  



2020年3月11日水曜日

dialogCornerRadius でダイアログの角丸具合を指定する

Android Pie(API Level 28)から ?android:attr/dialogCornerRadius でダイアログの角丸具合を指定できるようになりましたが、AppCompat や MaterialComponents では ?attr/dialogCornerRadius としてバックポートされています。
  1. <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  2.     <item name="dialogCornerRadius">12dp</item>  
  3. </style>  
(<item name="android:dialogCornerRadius">12dp</item> だと Android Pie(API Level 28)以降だけ角丸になります。)



<item name="dialogCornerRadius">12dp</item>

API Level 21



API Level 27



API Level 28





<item name="android:dialogCornerRadius">12dp</item>

API Level 21



API Level 27



API Level 28





2020年3月10日火曜日

Material Design Components for Android 1.1.0 でボタンのデフォルトカラーが colorAccent から colorPrimary に変わった

Theme.AppCompat.Light.DarkActionBar



Theme.MaterialComponents.Light.DarkActionBar (1.0.0)



Theme.MaterialComponents.Light.DarkActionBar (1.1.0)





何もしてないのに(MDC の version を 1.1.0 に上げたけど...) 色が!変わった!

ピンクはどこの色かというと colorAccent に指定している色です。では緑はどこの色かというと colorPrimary に指定している色です。

ボタン系のデフォルトカラーが 1.1.0 から colorPrimary に変わったようです。



AlertDialog のボタンの色は

?attr/materialAlertDialogTheme に指定されている
ThemeOverlay.MaterialComponents.MaterialAlertDialog

Base.ThemeOverlay.MaterialComponents.MaterialAlertDialog

Base.V14.ThemeOverlay.MaterialComponents.MaterialAlertDialog の
  1. <item name="buttonBarPositiveButtonStyle">@style/Widget.MaterialComponents.Button.TextButton.Dialog</item>  
  2. <item name="buttonBarNegativeButtonStyle">@style/Widget.MaterialComponents.Button.TextButton.Dialog</item>  
  3. <item name="buttonBarNeutralButtonStyle">@style/Widget.MaterialComponents.Button.TextButton.Dialog.Flush</item>  

Widget.MaterialComponents.Button.TextButton.Dialog の
  1. <item name="android:textColor">@color/mtrl_text_btn_text_color_selector</item>  

@color/mtrl_text_btn_text_color_selector
  1. <selector xmlns:android="http://schemas.android.com/apk/res/android">  
  2.   <item android:alpha="1.00" android:color="?attr/colorPrimary" .../>  
  3.   <item android:alpha="0.60" android:color="?attr/colorOnSurface" .../>  
  4.   <item android:alpha="1.00" android:color="?attr/colorPrimary" .../>  
  5.   <item android:alpha="0.38" android:color="?attr/colorOnSurface"/>  
  6. </selector>  
あー、colorPrimary と colorOnSurface になったのねぇ。

ちなみに 1.0.0 のときの @color/mtrl_text_btn_text_color_selector では colorAccent 使ってます。
  1. <selector xmlns:android="http://schemas.android.com/apk/res/android">  
  2.   <item android:color="?attr/colorAccent" android:state_enabled="true"/>  
  3.   <item android:color="@color/mtrl_btn_text_color_disabled"/>  
  4. </selector>  



ボタンの色を変えたいときは 「AlertDialog の Negative ボタンの文字色を変える」 と同じ感じでやればOK
  1. <style name="ThemeOverlay.MyApp.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">  
  2.     <item name="buttonBarPositiveButtonStyle">@style/Widget.MyApp.Button.TextButton.Dialog</item>  
  3.     <item name="buttonBarNegativeButtonStyle">@style/Widget.MyApp.Button.TextButton.Dialog</item>  
  4. </style>  
  5.   
  6. <style name="Widget.MyApp.Button.TextButton.Dialog" parent="Widget.MaterialComponents.Button.TextButton.Dialog">  
  7.     <item name="android:textColor">#1565C0</item>  
  8. </style>  
  1. AlertDialog.Builder(this, R.style.ThemeOverlay_MyApp_MaterialAlertDialog)  
  2.     .setTitle("Title")  
  3.     .setMessage("Message")  
  4.     .setPositiveButton(android.R.string.ok, null)  
  5.     .setNegativeButton(android.R.string.cancel, null)  
  6.     .show()  




2020年3月9日月曜日

Material Design Components for Android 1.1.0 から Checkbox で android:button を指定するなら app:useMaterialThemeColors="false" も必要(なことが多い)

Checkbox のマークをカスタマイズするときは android:button に drawable resource を指定します。

例えば以下のような drawable を用意して Checkbox の android:button に指定したのが次のスクリーンショットの下2つです。
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <layer-list xmlns:android="http://schemas.android.com/apk/res/android">  
  3.     <item  
  4.         android:bottom="7dp"  
  5.         android:left="7dp"  
  6.         android:right="7dp"  
  7.         android:top="7dp">  
  8.         <selector>  
  9.             <item android:state_checked="true">  
  10.                 <shape android:shape="oval">  
  11.                     <size  
  12.                         android:width="18dp"  
  13.                         android:height="18dp" />  
  14.                     <stroke  
  15.                         android:width="6dp"  
  16.                         android:color="#6666ff" />  
  17.                 </shape>  
  18.             </item>  
  19.             <item>  
  20.                 <shape android:shape="oval">  
  21.                     <size  
  22.                         android:width="18dp"  
  23.                         android:height="18dp" />  
  24.                     <stroke  
  25.                         android:width="2dp"  
  26.                         android:color="#cccccc" />  
  27.                 </shape>  
  28.             </item>  
  29.         </selector>  
  30.     </item>  
  31. </layer-list>  
  1. <CheckBox  
  2.     ...  
  3.     android:button="@drawable/checkbox"  
  4.     ... />  




全く同じコードで Activity の theme を Material Design Components for Android 1.0.0 に変えたのが次のスクリーンショットです。



同じようになってますね。

全く同じコードで Activity の theme を Material Design Components for Android 1.1.0 に変えたのが次のスクリーンショットです。



なんということでしょう!android:button で指定した drawable resource が Material Design の theme color で tint されるようになりました。tint で使われる色は colorControlActivated と colorOnSurface です。

この挙動を止めるには app:useMaterialThemeColors="false" を指定します。
  1. <CheckBox  
  2.     ...  
  3.     android:button="@drawable/checkbox"  
  4.     app:useMaterialThemeColors="false"  
  5.     ... />  




tint されなくなりました!



2020年3月6日金曜日

Facebook sdk で <meta-data> を設定しているのに初期化されていないと怒られる問題に対応した

いやー、まったくひどい落とし穴でしたよ。

もとの構成はこんな感じです(facebookAppId の値はダミーです)。

build.gradle
  1. android {  
  2.     ...  
  3.     defaultConfig {  
  4.         ...  
  5.         manifestPlaceholders = [facebookAppId: "1234567890000000"]  
  6.     }  
  7.     ...  
  8. }  
  9.   
  10. dependencies {  
  11.     ...  
  12.     implementation "com.facebook.android:facebook-android-sdk:6.1.0"  
  13.     implementation "com.facebook.android:facebook-share:6.1.0"  
  14. }  
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest ...>  
  3.   
  4.     ...  
  5.   
  6.     <application  
  7.         ...>  
  8.   
  9.         ...  
  10.   
  11.         <meta-data  
  12.             android:name="com.facebook.sdk.ApplicationId"  
  13.             android:value="${facebookAppId}" />  
  14.   
  15.         <provider  
  16.             android:name="com.facebook.FacebookContentProvider"  
  17.             android:authorities="com.facebook.app.FacebookContentProvider${facebookAppId}"  
  18.             android:exported="true" />  
  19.   
  20.     </application>  
  21.   
  22. </manifest>  

公式のドキュメント(https://developers.facebook.com/docs/android/getting-started#app_id)には meta-data を AndroidManifest に書けば勝手に初期化処理がされると書いてあるのに、実際に動かすと以下の初期化処理が走っていないというエラーで落ちます。

java.lang.ExceptionInInitializerError
     Caused by: The SDK has not been initialized, make sure to call FacebookSdk.sdkInitialize() first.


調べてみると FacebookSdk の loadDefaultsFromMetadata() の中で meta-data から読みんだ com.facebook.sdk.ApplicationId の値が Float になっていることが判明しました!(facebookAppId が全て数字だからそうなるのでしょうか...)

下記の Object appId が Float になってしまっていたのです。そのためその後の if 文に入らず、applicationId がセットされていませんでした。

FacebookSdk.java
  1. // Package private for testing only  
  2. static void loadDefaultsFromMetadata(Context context) {  
  3.     if (context == null) {  
  4.         return;  
  5.     }  
  6.   
  7.     ApplicationInfo ai = null;  
  8.     try {  
  9.         ai = context.getPackageManager().getApplicationInfo(  
  10.                 context.getPackageName(), PackageManager.GET_META_DATA);  
  11.     } catch (PackageManager.NameNotFoundException e) {  
  12.         return;  
  13.     }  
  14.   
  15.     if (ai == null || ai.metaData == null) {  
  16.         return;  
  17.     }  
  18.   
  19.     if (applicationId == null) {  
  20.         Object appId = ai.metaData.get(APPLICATION_ID_PROPERTY);  
  21.         if (appId instanceof String) {  
  22.             String appIdString = (String) appId;  
  23.             if (appIdString.toLowerCase(Locale.ROOT).startsWith("fb")) {  
  24.                 applicationId = appIdString.substring(2);  
  25.             } else {  
  26.                 applicationId = appIdString;  
  27.             }  
  28.         } else if (appId instanceof Integer) {  
  29.             throw new FacebookException(  
  30.                     "App Ids cannot be directly placed in the manifest." +  
  31.                     "They must be prefixed by 'fb' or be placed in the string resource file.");  
  32.         }  
  33.     }  
  34.   
  35.     ...  
  36. }  
ここの処理を読むと、appId の最初に fb or FB がついているときはそれを省いた部分を applicationId にしていることがわかります。

そこで以下のように meta-data の value を "fb${facebookAppId}" にしたら怒られなくなりました!
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest ...>  
  3.   
  4.     ...  
  5.   
  6.     <application  
  7.         ...>  
  8.   
  9.         ...  
  10.   
  11.         <meta-data  
  12.             android:name="com.facebook.sdk.ApplicationId"  
  13.             android:value="fb${facebookAppId}" />  
  14.   
  15.         <provider  
  16.             android:name="com.facebook.FacebookContentProvider"  
  17.             android:authorities="com.facebook.app.FacebookContentProvider${facebookAppId}"  
  18.             android:exported="true" />  
  19.   
  20.     </application>  
  21.   
  22. </manifest>  



ちなみに facebookAppId を string resource で用意した場合
  1. <string name="facebook_app_id">1234567890000000</string>  
とか
  1. android {  
  2.     ...  
  3.     defaultConfig {  
  4.         ...  
  5.         resValue "string", "facebook_app_id", "1234567890000000"  
  6.     }  
  7.     ...  
  8. }  
のときは、fb を付けなくても com.facebook.sdk.ApplicationId の値は String として読み込まれます。
  1. <meta-data  
  2.     android:name="com.facebook.sdk.ApplicationId"  
  3.     android:value="@string/facebook_app_id" />