- 投稿日:2020-03-14T18:39:19+09:00
Andorid Studioのインストールガイド
目次
- インストーラのダウンロード
- インストール
- Android Studioの初期設定
インストーラのダウンロード
以下のリンクからAndroid Studio 3.6.1のインストーラをダウンロードする。
https://developer.android.com/studioインストール
Android Studioの初期設定
- 上記のインストール完了後、以下のインポート設定のポップアップが表示されたら、「OK」をクリックする。
- 投稿日:2020-03-14T18:38:02+09:00
Android開発初心者の自分が躓いたところ
その1 SDKの差異
- Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (26.1.0) and test app (27.1.1) differ
どうやらSDKのヴァージョンの差異が原因のよう。
以下をbuild.gradle(Module:app)のdependencies内に追記。
androidTestCompile 'com.android.support:support-annotations:27.1.1' compile 'com.android.support.test:runner:1.+'その2 エミュレーターが動かない
- 色々解決方法があるようだが、実機で確認したほうが手っ取り早い。 動かなければ諦めろ。
その3 実機を認識しない
- スマホ側から設定→デバイス接続→USBの設定を確認し、「充電のみ」が設定されているので 「ファイルを転送」or 「写真を転送」に切り替える。
併せて、たまにSDKManagerから「Google USB Driver」をインストールし忘れることがあるので
必ずインストールしておくこと。その4 メインのプログラム(javaとかkotlinとか)でRがエラーになる
- 結構単純で、レイアウトを編集するstring.xmlやstyle.xml上にエラーが1つでも存在すると、ソース上のRは全てエラーになってしまうので、そちらで原因を探る。設定したidがメインプログラムと紐づいていないことが結構あった。スペルミスが大半。
最後に
- 環境構築で時間がかかり、実際のコードもいつものJavaより難しい気も。 Unityを用いて、ARアプリの開発もしてみたい。
参考にしたサイト
- 投稿日:2020-03-14T17:52:14+09:00
Github ActionsのAndroid CIでNDKを切り替える
概要
Github ActionsのAndroid CIを実行させた場合、Github Actionsで用意されているHost Runnerをそのままを使うと、virtual-environments/Ubuntu1804-README.md に記載されたバージョンのNDKが適用される。
例えば、2020年3月14日時点ではAndroid NDK 21.0.6113669
となる。ただ、場合によっては別のバージョンのNDKを利用したいケースもある。
NDKの切り替えに対応したandroid.yml
CircleCI Orb Registry - circleci/android を参考に、NDKを指定できるようにandroid.ymlを記述する。
なお、
NDK_SHA
とNDK_VERSION
は、こちらを参考に設定すればよい。name: Android CI on: pull_request: types: [opened] push: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name : install ndk run: | sudo curl --silent --show-error --location --fail --retry 3 \ --output /tmp/$NDK_VERSION.zip \ https://dl.google.com/android/repository/$NDK_VERSION-linux-x86_64.zip sudo echo "$NDK_SHA /tmp/$NDK_VERSION.zip" > /tmp/$NDK_VERSION.zip.sha1 sha1sum -c /tmp/$NDK_VERSION.zip.sha1 sudo rm -rf $ANDROID_SDK_ROOT/ndk-bundle sudo unzip -q /tmp/$NDK_VERSION.zip -d $ANDROID_SDK_ROOT sudo mv $ANDROID_SDK_ROOT/$NDK_VERSION $ANDROID_SDK_ROOT/ndk-bundle sudo chown -R root:root $ANDROID_SDK_ROOT/ndk-bundle sudo rm -f /tmp/$NDK_VERSION* if [[ -d $ANDROID_SDK_ROOT/ndk-bundle && \ -n "$(ls -A $ANDROID_SDK_ROOT/ndk-bundle)" ]]; then echo "Android NDK installed" else echo "Android NDK did not install successfully" exit 1 fi env: NDK_SHA: fd94d0be6017c6acbd193eb95e09cf4b6f61b834 NDK_VERSION: android-ndk-r19c - name: Build with Gradle run: ./gradlew buildGist
- 投稿日:2020-03-14T17:31:38+09:00
Android+RS-232C通信
はじめに
まだまだ計測機器と通信はRS-232Cが現役です。
ある計測機器(USB-miniB/RS-232C)とAndroidでRS-232C通信できないかと相談がありました。
そこでUSB-miniB/RS-232C(変換ケーブル)で通信できるか調査しました。先に結論を書くと、
- USB-TypeC→USB-TypeA変換→USB-miniBでは計測機器が認識できない端末あり。
- USB-TypeC→RS-232C変換ケーブル(ラトックシステム製)→RS-232CではUSB-TypeCの端末で認識できました。
調査環境
- Android(バージョンは後述)
- ある計測機器(USB-miniB/RS-232C)
- USB-TypeC→RS-232C変換ケーブル(ラトックシステム製)
USBシリアルコンバーター RS-USB60FC- usb-serial-for-android(計測機器を認識できたときに通信テスト)
usb-serial-for-android GitHub計測機器の認識調査
- UsbManager. getDeviceList()でUSB機器を検索します。
- 見つかったUSB機器情報をテキストに表示します。
- USB機器(計測機器/変換ケーブル)が見つからない場合、認識しないと判断します。
import android.os.Bundle import kotlinx.android.synthetic.main.activity_main.* import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager class MainActivity : AppCompatActivity() { private lateinit var usbManager:UsbManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) usbManager = getSystemService(Context.USB_SERVICE) as UsbManager btnTest.setOnClickListener { // USB機器を検索してテキストに表示 val devices = usbManager.deviceList var str = "" for (device in devices.values) { val name = device.deviceName str += "Name:${name}\n" str += "Vendor:{$device.vendorId}\n" str += "Product:{$device.productName}\n" str += "\n" } txtMsg.text = str } } }USB-miniBで調査
- USB-microB/Type-C→USB TypeA変換ケーブル→計測機器(USB-miniB)とAndroid端末の組み合わせで、計測機器を認識できる端末/できない端末があった。
- USB-Type-Cでは認識できる端末/できない端末があり、変換ケーブルと端末の選定が難しくなった。
計測機器を認識できる組み合わせ
- USB-microB→USB-TypeA→USB-miniB
端末 OS 認識 Nexus7 6.0.1 O Moto5G 7.0 O
- USB-TypeC→USB-TypeA→USB-miniB
端末 OS 認識 Pixel3a 10.0 O 計測機器を認識できない
- USB-TypeC→USB-TypeA→USB-miniB
端末 OS 認識 ZenfoneAR 7.0 X Zenfone5 8.0.0 X USB-TypeC→RS-232C変換ケーブル(RS-USB60FC)で調査
- 下記の全ての端末で認識できました。
- USB-TypeC→RS-USB60FC
端末 OS 認識 ZenfoneAR 7.0 O Zenfone3 Deluxe 8.0.0 O Zenfone5 8.0.0 O Nexus5X 8.1.0 O Essential Phone PH-1 10.0 O Zenfone5Z 10.0 O Pixel3a 10.0 O おわりに
- USB-TypeC→USB-TypeA変換→USB-miniBでは計測機器が認識できない端末あり。
- USB-TypeC→RS-232C変換ケーブル(ラトックシステム製)→RS-232CではUSB-TypeCの端末で認識できました。
- ラトックシステム製のRS-232C変換ケーブルは、USB-TypeA/miniB/microB/Type-Cのタイプが用意されているので、ターゲット端末に応じて使おうと思います。
- 投稿日:2020-03-14T16:59:58+09:00
[Android]Flow と Retrofit を組み合わせたサンプルと解説
はじめに
Flow と Retrofit を組み合わせたて次のサンプルを作成します。
アーキテクチャは Google が推奨している MVVM で作成を進めます。
作成するもの アーキテクチャ TL;DR
- Flow と Retrofit を連携するときは、戻り値が Flow となるように自分で実装する。
- Room から取得した Flow は
asLiveData
で LiveData に変換できる。- Flow を LiveData に変換したあとは、通常の LiveData と同じで Observe して利用する。
- MVVM で
LiveData<T>
とFlow<T>
を組み合わせるとこの構成になる。Setup
アプリケーションの作成に必要となる、
Koin・Retrofit・Flow・Coilのライブラリをインストールする。
ライブラリ バージョン 説明 Koin 2.1.3 DIライブラリ Retrofit 2.2.4 HTTPクライアントライブラリ Coroutines 1.3.4 非同期処理やノンブロッキング処理を行うためライブラリ Coil 0.8.0 画像を読み込むためのライブラリ dependencies { ︙ def koin_version = "2.1.3" implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android-scope:$koin_version" implementation "org.koin:koin-android-viewmodel:$koin_version" implementation "org.koin:koin-android-ext:$koin_version" def coroutines_version = "1.3.4" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" def retrofit_version ="2.1.0" implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" def coil_version = "0.8.0" implementation "io.coil-kt:coil:$coil_version" def lifecycle_version = "2.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" }Model
Retrofit を通して Qiita のユーザー情報を取得できるようにします。ユーザー情報は Qiita API の GET /api/v2/users/:user_id を利用して取得します。次のクラスを作成し、Retrofit を通して Qiita API を利用できるようにします。
役割 クラス名 役割 Entity User Qiita API のユーザー情報を定義するクラス Service QiitaService Qiita API を利用するためのサービスクラス Repository UserRepository QiitaService を利用してデータを取得するクラス User
Qiita ユーザー情報を格納するためのデータクラスを定義する。
data class User( val description: String, val facebook_id: String, val followees_count: Int, val followers_count: Int, val github_login_name: String, val id: String, val items_count: Int, val linkedin_id: String, val location: String, val name: String, val organization: String, val permanent_id: Int, val profile_image_url: String, val team_only: Boolean, val twitter_screen_name: String, val website_url: String )QiitaService
Retrofit で Qiita API の GET /api/v2/users/:user_id を利用できるようにする。詳しくは Retrofit の公式ドキュメント を閲覧してください。
interface QiitaService { @GET("/api/v2/users/{user_id}") fun getUser(@Path("user_id") user_id: String): Call<User> }UserRepository
QiitaService から取得した
Call<User>
を UserRepository でFlow<User>
に変換する。こんな感じで ViewModel からデータ取得をリクエストする際には、Call<User>
ではなくFlow<User>
を通して行うようにしてやる。class UserRepository(private val service: QiitaService) { fun getUser(userId: String): Flow<User> { return flow { try { emit(service.getUser(userId).execute().body()) } catch (e: Exception) { Log.e("UserRepository", "getUser error", e) emit(nullUser) } }.flowOn(Dispatchers.IO) } }ViewModel
ViewModel では Model から取得した
Flow<T>
をLiveData<T>
に変換し、 View がLiveData<T>
を購読してデータを表示できるようにしておきます。Flow<T>
はasLiveData()
でLiveData<T>
に変換できるので、asLiveData()
を利用して変換してやります。class MainViewModel(private val userRepository: UserRepository): ViewModel() { val user: LiveData<User> = userRepository.getUser("kaleidot725").asLiveData() }View
Koin
作成してきた ViewModel と Model を生成するため Koin の AppModule を定義する。
次の定義で QiitaService・UserRepository・MainViewModel を生成できるようにする。val appModule = module { single { Retrofit.Builder() .baseUrl("https://qiita.com/") .addConverterFactory(GsonConverterFactory.create()) .build() } single { get<Retrofit>().create(QiitaService::class.java) } single { UserRepository(get()) } viewModel { MainViewModel(get()) } }MainActivity
ここまで定義できれば、あとは MainActivity で View を更新する処理を記述すれば完成です。
MainViewModel のLiveData<T>
をobserve
し、データ取得が完了したら View が更新されるようにします。class MainActivity : AppCompatActivity() { private val viewModel : MainViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) startKoin { androidLogger() androidContext(applicationContext) modules(appModule) } val binding : ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = viewModel viewModel.user.observe(this, Observer { binding.userImageView.load(it.profile_image_url) binding.userNameValue.text = it.name binding.idValue.text = it.id binding.organizationValue.text = it.organization binding.descriptionValue.text = it.description }) } }<?xml version="1.0" encoding="utf-8"?> <layout> <data> <variable name="viewModel" type="kaleidot725.sample.ui.MainViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout 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=".ui.MainActivity"> <ImageView android:id="@+id/user_image_view" android:layout_width="250dp" android:layout_height="250dp" android:layout_marginTop="32dp" android:background="@android:color/black" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_margin="32dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/user_image_view"> <TextView android:id="@+id/user_name_title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="User Name" android:textSize="16sp" app:layout_constraintEnd_toStartOf="@id/user_name_value" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="User Name" /> <TextView android:id="@+id/user_name_value" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/user_name_title" app:layout_constraintTop_toTopOf="parent" tools:text="Yusuke Katsuragawa" /> <TextView android:id="@+id/id_title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="ID" android:textSize="16sp" app:layout_constraintEnd_toStartOf="@id/id_value" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/user_name_title" tools:text="ID" /> <TextView android:id="@+id/id_value" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/id_title" app:layout_constraintTop_toBottomOf="@id/user_name_value" tools:text="ID" /> <TextView android:id="@+id/organization_title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="Organization" android:textSize="16sp" app:layout_constraintEnd_toStartOf="@id/organization_value" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/id_title" tools:text="Organization" /> <TextView android:id="@+id/organization_value" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/organization_title" app:layout_constraintTop_toBottomOf="@id/id_value" tools:text="Company Name" /> <TextView android:id="@+id/description_value" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="32dp" android:textSize="18sp" app:layout_constraintTop_toBottomOf="@id/organization_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" tools:text="Description" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>おわりに
アプリケーションは Model
Flow<T>
を生成、 ViewModel でLiveData<T>
を生成、そして View でLiveData<T>
を observe してデータ更新という構成で実装しました。Room だと Room 側でFlow<T>
に変換してくれるのですが、Retrofit では対応していなそうなので自身でFlow<T>
に変換しなくてはならないみたいですね。エラー処理を考慮するとまた印象が変わるかもしれませんが、
LiveData<T>
とFlow<T>
を組み合わせたパターンはとても実装しやすいですね。今後はLiveData<T>
とFlow<T>
の組み合わせが主流になるんではないでしょうか。今回作成したサンプルは次に保存してありますので興味があれば閲覧お願いします。
- 投稿日:2020-03-14T16:04:25+09:00
【Android】Epoxyのモデル変更判定
はじめに
RecyclerViewを超絶簡単に実装できるEpoxyだけど、こいつを使い初めてから幾度となく値を変更しても見た目が変更されない不具合に悩まされ続けてきた。
その度になんとなく他の部分と同じように修正してどうにかしてきたけど、先日やっと根本的な原因がわかった。
もはや自分では忘れないと思うけど、何故か調べても同じようなことで悩んでいる人の記事とか出てこなかったので紹介する。やりたいこと
選択したやつの色を変えてあげたい。
SNSのいいねボタンとか想像してもらえるとわかりやすいと思う。
押すたびにステータスが変わって、それに応じて見た目も変化して欲しい。実装方法
特別な処理は入れていないけど、一応のっけとく。
◾︎SampleActivity
class SampleActivity : AppCompatActivity() { private lateinit var binding: ActivitySampleBinding private var epoxyController: SampleEpoxyController? = null private var sampleEpoxyModels: List<SampleEpoxyModel>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_sample) setController() } private fun setController() { sampleEpoxyModels = (0..9).map { SampleEpoxyModel(it) } epoxyController = SampleEpoxyController( onClick = { val sampleEpoxyModel = sampleEpoxyModels?.find { sampleEpoxyModel -> sampleEpoxyModel.id == it.id } sampleEpoxyModel?.isSelected = sampleEpoxyModel?.isSelected?.not() ?: false epoxyController?.setData(sampleEpoxyModels) } ) binding.recyclerView.setController(epoxyController as EpoxyController) epoxyController?.setData(sampleEpoxyModels) } }◾︎SampleController
class SampleEpoxyController( private val onClick: (SampleEpoxyModel) -> Unit ) : TypedEpoxyController<List<SampleEpoxyModel>?>() { override fun buildModels(data: List<SampleEpoxyModel>?) { data?.forEach { sampleEpoxyModel -> itemSampleList { id("EpoxyModel_id:${sampleEpoxyModel.id}") sampleEpoxyModel(sampleEpoxyModel) onClick { _ -> onClick.invoke(sampleEpoxyModel) } } itemDivider { id("divider") } } } }◾︎ activity_sample.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.airbnb.epoxy.EpoxyRecyclerView android:id="@+id/recycler_view" android:layout_width="0dp" android:layout_height="0dp" android:splitMotionEvents="false" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>◾︎ item_sample_list.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="sampleEpoxyModel" type="com.kl.testapp2.epoxymodelchange.SampleEpoxyModel" /> <variable name="onClick" type="android.view.View.OnClickListener" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="80dp" android:background="@{sampleEpoxyModel.isSelected ? @color/colorPrimary : @color/white}" android:onClick="@{onClick}" /> </layout>リストに設定した
onClick
のイベントを受け取って、Activityが保持しているListに対してfindして更新するアイテムを見つける。
取得したアイテムのフラグを反転させて再度setData()
を実行している。不具合内容
中の値を入れ替えて、しっかり
setData
しているのに見た目が変わらない。sampleEpoxyModel?.isSelected = sampleEpoxyModel?.isSelected?.not() ?: false epoxyController?.setData(sampleEpoxyModels)更新後の値を確認してもやっぱりきちんと変わっている。
一応idを変えればViewHolderごと再生性できるんだけど、それをするとちらついて鬱陶しい。原因&解決方法
色々試した結果、リストのフラグは変わっていても、インスタンス自体は同じなのが原因だということがわかった。
どうやらEpoxyは、保持しているリストの中の値が変わったかどうかは見ていないようで、参照しているインスタンスが変更されたかどうかで変更を検知してるみたい。
==
とequals()
の違いだね。ということは、変更する要素のindexに対して
.copy()
で新たに生成したインスタンスを入れてあげれば変更が検知されるはず。onClick = { val index = sampleEpoxyModels?.indexOf(it) ?: -1 sampleEpoxyModels?.set( index, it.copy( isSelected = it.isSelected.not() ) ) epoxyController?.setData(sampleEpoxyModels) }こんな感じで修正したら、きちんと動きました!!
まとめ
Epoxyでフラグ使ってViewの見た目を更新するときはインスタンスごと再生成してあげましょう!
おわり
- 投稿日:2020-03-14T14:39:34+09:00
Kotlin Coroutine 入門1: 起動と suspend
Kotlin 標準の並行プログラミング API である coroutine を理解1したのでまとめました。
本家のガイド は包括的で(上から読めば)丁寧に書いてあるのですが、実際に自分が読んだ際には理解に結構苦労したので、少し別のアプローチでの入門になります。
(はじめにお断り: Coroutine と Promise, async/await, Rx, etc…)
並行処理の API といえば、ここ数年だと「Promise」や「Rx」のような計算結果を表すオブジェクトを
then()
やmap()
などでつなげていくスタイルか、「async/await」などと呼ばれるような計算結果オブジェクトを馴染みやすい形で書くスタイルが多いと思います。coroutine の基本を学習する際には、一旦これらは忘れてもらった方が良いです。 なんとなく async/await っぽい位置づけと考えている方もいるかもしれません(筆者もそう思っていた一人です)。馴染みやすい書き方という意味では似ているのですが、背後の考えは結構違います。
Coroutine と suspend 関数
簡単な例
以下、簡単な使用例として通信処理を伴う処理を考えます。同期的に呼び出すとこうなると思います。
// メインスレッドで実行 fun runMain() { val data = Weathers.tomorrow() println("明日の天気: ${data}") } fun Weathers.tomorrow(): String { Thread.sleep(2000) // 長い処理を表すための仮実装 return "晴れ" }これを実行すると、2秒後に
明日の天気: 晴れ
が出力されます。一方 coroutine を使うと以下のように書く事ができます2。// メインスレッドで実行 fun runMain(): Job = viewModelScope.launch { val data = Weathers.tomorrow() println("明日の天気: ${data}") } suspend fun Weathers.tomorrow(): String { delay(2000) // 長い処理を表すための仮実装 return "晴れ" }
viewModelScope.launch { … }
を呼び出す事でバックグランドで処理が開始されます。suspend
,delay
などの見慣れないキーワードが一部追加されていますが、主な記述内容は同期的な呼び出しと同じです。最初の同期的な書き方では2秒間スレッドがブロックされてしまうという問題があります。この間ユーザーの操作がフリーズしてしまうのでアプリケーションでこのコードを書くのは現実的ではありません。一方 couroutine を使うと結果が出るまでの2秒間ユーザーは普通に操作ができます。便利!!??
処理の中断(suspend)
上のような差ができる理由は coroutine ではスレッドをブロックする代わりに処理を「中断」するからです。
最初の例では
Thread.sleep()
により2秒間メインスレッドを占有(ブロック)していて、その間他の処理がしたくてもメインスレッドが使えません。一方 coroutine の例で使ったdelay()
関数は2秒間ブロックするのではなく、メインスレッドを2秒間解放してからメインスレッドを再取得して処理を続行します。開放中は他の処理にスレッドを活用できます。これを「中断(suspend)」と呼びます。フレームワークやライブラリの機能を使って同じような事を実現する事ためにはコールバック関数を用意する必要があるため、coroutine と比べると周りくどい書き方になりがちです。以下は Android による例です。
// メインスレッドで実行 fun runMain() { // 結果をコールバックを渡して呼び出し Weathers.tomorrow { data -> println("明日の天気: ${data}") } } fun Weathers.tomorrow(callback: (String) -> Unit): Unit { // 2秒後に処理を呼び出すようタイマーを設定する。 // callback 処理はメインスレッドで行われるが、それまでの間スレッドは解放される val handler = Handler() handler.postDelayed({ callback("晴れ") }, 2000) }
delay()
のように中断が起こる関数を「suspend 関数」と呼びます。通常の関数は中断しない(できない)ので、suspend 関数を呼び出す事ができません。 suspend 関数を呼びたい場合はlaunch
の中で呼び出すか、関数にsuspend
キーワードを付けて suspend 関数にする必要があります。CoroutineScope
coroutine を使った例では
viewModelScope
に対してlaunch { … }
を呼び出す事で coroutine を起動していました。coroutine を起動する機能を持つオブジェクトは「CoroutineScope」と呼ばれます。CoroutineScope は coroutine の起動だけではなく起動した coroutine を適切に終了する役割を担っています。例えば Android の KTX で提供されている3
viewModelScope
は、画面を閉じた時に coroutine を自動でキャンセルしてくれます。 Android でユーザー操作による処理を実行する場合はviewModelScope
から起動すると良いでしょう。一方で Kotlin の標準で用意されている
GlobalScope
もあります。こちらは処理が自動でキャンセルされません。一般的には自動でキャンセルを行ってくれる scope を使った方が良いとされます4。coroutine を手動でキャンセルする事も可能です。
launch { … }
メソッドの結果に対しcancel()
を呼び出すとキャンセルする事ができます。fun runMain(): Job = viewModelScope.launch { val data = Weathers.tomorrow() println("明日の天気: ${data}") } val job = runMain() // 個別にジョブをキャンセルする。特に必要な場合のみ使う。 job.cancel() runMain() runMain() // キャンセルすれば viewModelScope から起動したジョブを全てキャンセルできる。 viewModelScope.cancel() // しかし実際はフレームワーク側でやってくれるので不要Coroutine は難しくない
このように、coroutine の基本は「
launch()
で非同期処理を開始し、中断する関数にはsuspend
をつける」というとてもシンプルなものです。並行プログラミングをする上で見落としがちなキャンセル処理も、CoroutineScope が適切にやってくれるようになっています。他のプラットフォームにおける Promise のような概念もあるのですが、特に必要ない限りは suspend 関数で済ませるのが簡単で問題も少ないです。
とはいえ suspend 関数が充実していない段階では、suspend 関数だけで済ませるのは中々難しいと思います。Kotlin では従来の処理を suspend 関数にする方法をいくつか用意しているので、それを紹介します。
suspend 関数への変換
ブロック処理 → suspend 関数
一定期間待つような単純な処理であれば
delay()
という組み込みの suspend 関数を使えば実現できました。しかしそう言った suspend 関数がない場合は従来のブロックする処理から suspend 関数を作る必要があります。一番最初の例にあったブロック版の関数を考えてみましょう。
fun Weathers.tomorrow(): String { Thread.sleep(2000) // 長い処理を表すための仮実装 return "晴れ" }以下のように別のスレッドに切り替える事で、ブロック処理を suspend 関数に変換する事ができます。
suspend fun Weathers.tomorrow(): String = withContext(Dispatchers.IO) { Thread.sleep(2000) // 長い処理を表すための仮実装 "晴れ" }
withContext(Dispatchers.IO) { … }
で別のスレッドを使って実行し、withContext()
の処理が終わるまでの間メインスレッドを開放する事ができます。標準では
Dispatches.IO
(IO処理用) の他にDispatchers.Main
(メインスレッド5)やDispatchers.Default
(計算用)があります。また、独自でスレッドプールを作る事もできます。スレッドの切り替えについての詳細は、公式ドキュメントの Coroutine Context and Dispatchers に書かれてあります。
コールバック関数 → suspend 関数
すでに非同期処理用にコールバック形式の関数がある場合は、どうでしょう。先ほどの
Handler
を使った例を改善してみます。fun Weathers.tomorrow(callback: (String) -> Unit): Unit { // 2秒後に処理を呼び出すようタイマーを設定する。 // callback 処理はメインスレッドで行われるが、それまでの間スレッドは解放される val handler = Handler() handler.postDelayed({ callback("晴れ") }, 2000) }
suspendCoroutine { … }
を使う事で suspend 関数に変換する事ができます。suspend fun Weathers.tomorrow(): String = suspendCoroutine { c -> // 2秒後に処理を呼び出すようタイマーを設定する。 // callback 処理はメインスレッドで行われるが、それまでの間スレッドは解放される val handler = Handler() handler.postDelayed({ c.resume("晴れ") }, 2000) }ただし、
suspendCancellableCoroutine
を使った方が望ましいです。suspend fun Weathers.tomorrow(): String = suspendCancellableCoroutine { c -> // キャンセル対応用コード1: コールバック処理を一旦変数で持つ val callback = Runnable { // キャンセル対応用コード2: キャンセルされている場合は何もしない if (c.isActive) c.resume("晴れ") } val handler = Handler() // キャンセル対応用コード3: coroutine のキャンセルが起きた時に Handler 側もキャンセルする c.invokeOnCancellation { handler.removeCallbacks(callback) } // 2秒後に処理を呼び出すようタイマーを設定する。 // callback 処理はメインスレッドで行われるが、それまでの間スレッドは解放される handler.postDelayed(callback, 2000) }ちょっと長くなってしまいましたが、これは主にキャンセル対応のためのコードがあるからです。coroutine は任意のタイミングでキャンセルする事ができるので、キャンセルされた時に元の非同期関数の処理もキャンセルする事が望ましいです。また、処理の要所要所でキャンセルされていないか(
isActive
が true か)チェックする必要もあります。キャンセルと協調的マルチタスキング
「キャンセル対応」というのが出てきました。 suspend 関数へ変換する処理を適切に作るためには、coroutine のキャンセルの仕組みを理解した方が良いでしょう。
実は、CoroutineScope 等に対してキャンセルを呼び出してもいきなり実行中の処理が打ち切られるわけではありません。実際に打ち切られるポイントは限られており、またキャンセルに従わないで処理を続行する事も可能です。
キャンセルされた処理が実際に打ち切られるポイントは、主に「suspend 関数を呼んだタイミング」です。キャンセル対応された suspend 関数6はキャンセル対応しています)を呼び出した場合、
CancellationException
が呼ばれます。投げられたCancellationException
をキャッチせずに coroutine を終了すれば、キャンセル処理が完了します。そのため、suspend 関数が
CancellationException
を投げるかもしれない事を想定するだけでキャンセル対応できます。一方で、ブロック処理を suspend 関数を作るようなケースだと、isActive
やyield
でキャンセルが要求されているか確認したりするなどして、明示的にキャンセル対応しないと最後まで処理を続けてしまいます。キャンセルについての詳細は、公式ドキュメントの Cancellation and Timeouts に書かれてあります。
まとめ
ここまで Kotlin の coroutine の基本、特に起動と中断について解説しました。「時間のかかる処理をスレッドをブロックせずに非同期で行いたい」といった用途であれば、今回の内容で大体事足りるのではないかと思います。
次回は並列処理を行う際の話や、Kotlin の coroutine で特徴的な structured concurrency について解説したいと思います。
入門読んで Hello, world! ができるようになった程度という意味。Flow も分かってないし…。 ↩
Android と KTX を使った場合の書き方。 ↩
Android の場合は
viewModelScope
の他にlifecycleScope
が用意されています(2020/03月時点)。 ↩Kotlinの標準ライブラリ開発のリーダーである Roman Elizarov さんによるThe reason to avoid GlobalScope に GlobalScope を避けるべき理由が書かれてあります。 ↩
Dispatchers.Main
の実装はプラットフォーム側が用意するもので、素の Kotlin では使用できません。 ↩標準の suspend 関数(
delay()
など)は全てキャンセル対応しています。 ↩
- 投稿日:2020-03-14T14:23:39+09:00
FlutterのWebView内からメールアプリを起動してみた件
はじめに
みなさんはFlutterでWebViewを使用したことありますか?
すみません、そんなことはどうでもいいです。今回はタイトルにもある通り、FlutterのWebView内から端末内のメールアプリを起動してみます。最終結果
最終的に、下の画像みたいにメールアプリを起動する画面を出します。
いつメールアプリを起動するのか
普通にWebViewを出してるだけだと、メールアプリを起動する場面はなかなかありませんが、あるとするなら、「お問合せはコチラ」みたいなとこクリックしてメールアプリを起動するみたいな感じです!
掲示板みたいに、自分の名前とかメアドを入力してお問合せできるページが用意されていれば、特に問題はないんですけど、全部がそういうわけじゃないからな~~~普通にWebViewからメールアプリを起動してみる
普通にWebViewからメールアプリを起動してみます。Flutterにはなぜか最初からWebViewは入っていないため、ライブラリを用います。今回使用するライブラリは「webview_flutter」です。
また、メールアプリを起動するものには、urlに「"mailto:メアド"」がはいってます。(例:mailto:example@ex.com)今回開くURLは私が鹿児島大学で所属している「KADAI INFO」のサークル紹介ページへアクセスします。(よかったらKADAI INFO見てほしい!)
main.dartWebView( initialUrl:"https://kadai-info.com/2020/03/13/kabadiclub-2020/", // URL読み込み javascriptMode: JavascriptMode.unrestricted, // javascriptの有効化 );ここは、普通のページなので、特に問題なく表示されます。しかし、この「入部希望の連絡をする」をタップしてみると以下のようになります。
はい、なんやらエラーページがでました。urlを見てみると、「mailto:〇〇@gmail.com」とメールアプリを起動するURLが設定されています。
パソコンには既にこういうアドレスを開く機能が備わっていますが、アプリでは自分で設定しないとこのようなエラーがでます。url_launcherを使う
こういったメールアプリを起動するような特殊なURL処理をするライブラリとして「url_launcher」というものがあります。
しかし、こちらのライブラリの例題は、WebView上で機能するものではなく、自分でボタンやら設定して、それがタップされたら起動する~みたいなやつです。
使い方はめちゃくちゃ簡単で、WebViewとほぼ同じ感じです(下のやつを参考にしてくれい!)。hoge.dartconst url = "指定のURL"; // 有効なアドレスだったら if (await canLaunch(url)) { await launch(url); // メールアプリを開く // 有効なアドレスでないなら } else { throw 'Could not launch $url'; }しかし、これを使用するためにはURLを知っておく必要があります。1つしかないなら直接打ち込めばいいですが、このサークルページは他にもいくつもあり、全部あらかじめ用意しておくのはマジきつい。
そこで、WebViewからURLを取得します。WebViewからURLを取得する
そもそもWebViewからどうやって、URLを取得するのか?
webview_flutterの中の「NavigationDeligate」から取得できます。これは、URL先に遷移するか制御するものです。また、これを使用するときは引数に「NavigationRequest」を渡します。つまり、こいつは遷移先のURLをどこかに保持している可能性があります。ライブラリを見てみると案の定「String url」を持っていたので、こいつにアクセスしてURLを取得します。hoge.dartnavigationDelegate: (NavigationRequest request) { String url = request.url; // request.urlで取得できる(requestの名前は何でもいい) }これで、URLは取得できましたので、あとはこのURLとurl_launcherを使用するだけでOKです!
メールアプリを起動させてみる
では、最後に実際に起動させてみます。コードは以下
main.dart_launchURL(String url) async { // 有効なアドレスだったら if(await canLaunch(url)){ await launch(url); // メールアプリを起動 return true; } // 有効なアドレスでないなら else{ return false; } } return WebView( initialUrl: url, navigationDelegate: (NavigationRequest request) { // メールアプリを起動する必要があったら if(_launchURL(request.url)){ return NavigationDecision.prevent; // 前のページに戻る } // メールアプリを起動する必要がなかったら else{ return NavigationDecision.navigate; // そのままページを読み込む } }, );メールアプリを起動したら前のページに戻る処理をしていますが、これはエラーページを表示させないためです。
これしないと↓みたいになって気持ち悪い。
だから、メールアプリを起動する必要があるようなページだった場合は、前のページに戻って、そうでないならそのまま突き進む処理を施したってわけだぜ!
まあ、最初にも載せてるけど、戻れば↓みたいに遷移前の画面でメールアプリ起動の画面を出せる終わりに
今回はメールアプリを起動させてみました。ていうか、メールアプリって最初から言ってるけど正しい言い方は分からんから別に指摘しないでくれ!頼む!フリじゃない!
また、自分で調べて日本語記事がないやつあったら書いていきます!ではまた!
- 投稿日:2020-03-14T14:09:53+09:00
61st聖光祭 アプリ局 AndroidJavaの教科書
1. AndroidStudioの操作一覧
英語さえ読めれば大丈夫だと思うが、一応まとめとく。
1.1 プロジェクトのつくり方
- ホーム画面に行く。
Start a new Android Studio project
をクリックEmpty Activity
を選択Next
をクリックName
を入力 (英語 全単語頭文字大文字 その他小文字 記号特殊文字禁止)Package Name
を入力 (英語 単語間は.
全文字小文字 特殊文字禁止)Language
はJava
固定Minimum API level
はお好みで。(23推奨)Finish
をクリック- ビルドを待つ
1.2 画面の見方
ファイルツリー
左にはファイルツリーが表示される。
-
ボタンで非表示に。非表示後はさらに左の1:Project
をクリックで表示できる。デバッグ関係
下にはデバッグやビルドをしたときのログが表示される。実行時は勝手に表示されるので作業中は必要ないと思われ。ログ以外にも補助ソフトをいれると
CheckStyle
やFind-Bug
も表示されるが、これらの切り替えはさらに下のタブでできる。TODO
機能はぜひ使ってほしい。プロセスステータスバー
デバッグ関係の下の下。右側には左から
行番号:左からの文字数
、文字コード、インデントのスペース数、ロック、フィードバック二つ、なんかが表示されている。これらは使わないでくれ(切実)。ロックは押すと編集不可になり、文字を入力するとダイヤログがでる。問題なのは通常では何もないがバーのまんなかと右側である。ビルドすると真ん中には現在何を行っているか表示され、右側にはプロセスバーが現れる。これがあるときはファイルの編集しない方がいい。
ツールバー(OS標準)
ウィンドウの一番うえの左から
File
、Edit
...と続いているツールバー。ほとんどのソフトに存在するOSが描画した(プログラミング脳)ツールバーである。かなり使うのでツールバーといわれたらここを見てほしい。ショートカットツールバー(AS装備)
ツールバーの下にあるAndroidStudio固有のツールバー。左にいま開いてるファイルの階層が表示される。階層をまたぐ移動はのちほど紹介するファイルタブではなくここからやるといい。右にはユーザーが編集できるショートカットがある。
▶
は実行、虫みたいなマークはデバックである。ここで注意なのがデバックを使うことである。実行はエラーの詳細、現状のログを出してくれない。実行はアプリの動画を撮ったりしたり完成品で遊ぶときに使用しよう。他にも上級者にはありがたい機能のショートカットがあるが、割愛する。メイン画面
メインの作業画面。上部にはファイルタブがあり、複数の(開かれている)ファイルを移動できる。xmlのときはそれ用の画面になる。他にも画像を開くと編集画面になったり、音声や動画を開くと再生バーがでてきたりする。
Palette-パレット(xml)
xmlを開くとメイン画面の左上に表示されるView一覧。ここから使うViewをとってくる。
Component Tree-コンポネントツリー(xml)
xmlを開くとメイン画面の左下に表示される配置済みのView一覧。PaletteからViewをここにドラッグしよう。あとは階層と上下関係を意識してレッツ!ドラッグアンドドロップ!(ガヴリ―ルドロップアウトみたいだよね←どこが)
メイン画面(xml)
Android画面が表示されてるところ。ここいじることは少ない。ちなみに右に表示されてる青い画面は領域の境とか大きさとか書かれているので参考にするといい。あとは、Android画面の左下のなぞの
//
はドラッグすると画面サイズを変えられる。(結構これすごい)また、上部にはたくさんの項目があるが、端末の向きを変えたり画面を本当に存在する端末(Googleが監修した端末のみ)のプリセットを利用できる。(Nexus5!!!)ほかはあんまりいじんないほうがいいよ(60回の聖光祭でやらかしました)
Attributes-アトリビュート(xml)
Viewの属性(パラメータ)を変更する画面。右側に縦長にあるやーつ。編集したいViewを選択してから変えようね?パラメータはViewによって違うし、多すぎるので覚える必要はない。英語よめ。
1.3 Androidのディレクトリ事情
Pathはファイルツリーでのパス。JavaはJava内での表記方法。xmlはxml内での表記方法。
ちなみにファイル名
は拡張子
を含みません。Javaファイル
Path:
app/java/パッケージ名/Jファイル名
Java:パッケージ名.Class名
xml :パッケージ名.Class名
画面を設計するxmlファイル
Path:
app/res/layout/ファイル名
Java:R.layout.xmlファイル名
xml :なし素材の画像
Path:
app/res/drawable/ファイル名
Java:R.drawable.ファイル名
xml :@drawable/ファイル名
その他の素材(動画とか音声とか)
Path:
app/res/row/ファイル名
Java:R.row.ファイル名
xml :@row/ファイル名
rowフォルダは新しく作る。その他のxml(string.xmlとか)
Path:
app/res/values/ファイル名
Java:R.ファイル名.
xml :@ファイル名/
ちょっとした例
string.xml<resources> <string name="hayaku_tugi_coi">Auribus oculi fideliores sunt.</string> </resources>このとき、
@string/hayaku_tugi_coi
となる。1.4 Github関連(Githubにつながっている場合)
リモートレポジトリを作らないとGithubは利用できないので、新規のプロジェクトには使えません。
Commit
こまめに結果にコミット!
1. ツールバーのVCS
をクリック
2.Commit
をクリックまた、ショートカットのチェックマークでもできます。
Push
- ツールバーの
VCS
をクリックGit
にホバーするPush
をクリック
- 投稿日:2020-03-14T13:04:05+09:00
AsyncTaskをTHREAD_POOL_EXECUTORで実行した際のPoolサイズ
AsyncTaskをexecuteOnExecutor(THREAD_POOL_EXECUTOR)で実行した際のPoolサイズについて調べました。
結果
THREAD_POOL_EXECUTORのPoolサイズはCPUの数で決まり、PoolサイズはMath.max(2, Math.min(CPU_COUNT - 1, 4))となります。
詳細
THREAD_POOL_EXECUTORは、AsyncTaskで生成されたThreadPoolExecutorで、生成時のパラメータは下記の通りです。
CcorePoolSize:Math.max(2, Math.min(CPU_COUNT - 1, 4))
maximumPoolSize:CPU_COUNT * 2 + 1
※ workQueuのサイズは128AsyncTask.javaprivate static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); // We want at least 2 threads and at most 4 threads in the core pool, // preferring to have 1 less than the CPU count to avoid saturating // the CPU with background work private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; private static final int KEEP_ALIVE_SECONDS = 30; private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128); public static final Executor THREAD_POOL_EXECUTOR; static { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); threadPoolExecutor.allowCoreThreadTimeOut(true); THREAD_POOL_EXECUTOR = threadPoolExecutor; }ThreadPoolExecutorのmaximumPoolSize
ThreadPoolExecutorのPoolサイズは、corePoolSizeおよびmaximumPoolSizeによって設定された境界に従って、自動的に調整されます。
corePoolSizeを超えているが、maximumPoolSizeスレッドが実行されていない場合、キューがいっぱいの場合にのみ新しいスレッドが作成されます。
※ THREAD_POOL_EXECUTORのworkQueuのサイズは128
- 投稿日:2020-03-14T08:37:52+09:00
AndroidとmacOSでファイル共有する原始的な方法
macOS使っているのに、iPhoneよりもAndroidが好きというエンジニアは多いと思います。AndroidならTermuxやいろいろ使えます。
それで、AndroidとmacOSで最も一番簡単にファイル共有する方法が分かったのでメモしておきます。それは、FTPやSSHでファイルのやりとりをするんです。なーんだという感じですが、広告がいっぱいのファイル共有アプリなんか使うより確実で、SSH使えば安全です。
Ftp Serverを使う
簡単な広告が出ますが、Android上でFTPサーバーを起動する一番簡単な方法です。
Android上でこれでオンにして、macOSのFilezilla ClientなどのFTPクライアントでアクセスします。開発者であれば最初からインストールされていることでしょう。
なお、同作者がSSH Serverも公開しているのですが、手元のAndroidでは接続できませんでした。
TermuxでSSH/SFTPを使う
少し前に、AndroidのターミナルエミュレーターTermuxについて書きました。Termuxを設定する際、MacからSSHにも接続するように設定しました。sshdをインストールして、Mac側で公開鍵を作り、Termuxに登録します。すると、上記で紹介したFilezillaを利用して、Androidにファイルが転送できます。
改めて、手順を整理すると以下のようになります。
- (1) ストアでTermuxをインストール
- (2) Termuxを起動して、
termux-setup-storage
を実行- (3) 必要なアプリをインストール
pkg update && pkg install git vim wget tmux
- (4) sshdをインストール
pkg install sshd
- (5) Macでターミナルを起動して公開鍵を作る
ssh-keygen -t rsa
- (6) Macのid_rsa.pubの内容をLINEやメールでAndroidに送り、
~/.ssh/authorized_keys
に追記する(なければ作る)。以上で、設定が完了です。以下の手順で、Filezillaを使ってファイルの転送ができます。
- (1) sshdを起動
sshd
- (2) TermuxでAndroidのIPアドレスを調べる
ifconfig | grep 192
- (3) Mac側でsshdに接続してみる
ssh hoge@192.168.xxx.xxx -p8022
(xxxには上記のIPアドレス)- (4) 正しく接続できたなら設定完了。FilezillaでAndroidに接続すると、安全にファイルの送受信が可能。
二回目以降は設定が要らないので、そこそこ気軽に使えます。
その他の方法
コメント欄で教えていただいた方法ですが、Android StudioのDevice File Managerを使ったファイル送受信も良いですね!
他にも、開発者に向けた手軽な方法があれば、コメント欄で教えてください!
まとめ
手軽なのは、Ftp Serverを使ってファイルを転送することですが、平文でファイルを転送しないといけないので、ちょっと心配です。そこで、Termuxでsshdを起動して、SFTPでファイル転送すれば安心して転送できます。ただし、Termuxでコマンドを打つのはそこそこ面倒なので、使い分けると良いでしょう。
- 投稿日:2020-03-14T00:51:19+09:00
kotlinでフェードイン、フェードアウトを実装した話
qiitaが「いいね」からLGTM(Look Good To Me)に変わりましたね。
くだらない上にただの雑談が数百いいねついてる現状を見てたらそりゃもう運営ナイスです
くだらん雑談で技術なしでバズらせたいならツイッターやってろ三流がって話。今回はフェードイン、フェードアウトを実装しました。
できることとしては
チュートリアルなんかでよくある、ゆったりふわーと真っ白い画面からチュートリアルにうつって、ゆったりふわーっと真っ白い画面から元いた画面に戻るあれです(伝われ)Animationってライブラリがあって数秒かけて透過させる的な処理はできるのですが、連打したら透過させる処理を繰り返してしまったりそのままでは使い物にならなかったので。
①fade outを時間を指定してできる(3秒経過後に関数を実行したかったができなかったからその機能を追加した。例えばfade out後にdialogFragmentをdismiss()したいなど)
②fade in, fade out中に他のanimationの処理が割り込まないようにisAnimagingというフラグを用いた(連打時のバグ防止)ではまずはdialogFragmentを表示するactivityから
MainActivity.ktclass MainActivity : AppCompatActivity(), BaseFullScreenDialogFragment.Callback { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) button.setOnClickListener { showFullScreenDialog() } } override fun onClickScreen() { comeBack() } private fun showFullScreenDialog() { // 付け足したければ val bundle = Bundle() val dialog = FullScreenDialogFragment.newInstance(bundle) dialog.setOnCallBack(this) dialog.show(supportFragmentManager, "dialog") } private fun comeBack() { // ダイアログを閉じたあとはここを通ります val a = 334 } }buttonってのはfade inのトリガーになるボタンです。
dialogでフルスクリーンのチュートリアルを表示しています。
setOnCallBackでは画面のタップを受けたらdismiss()するっていう処理を書いています。で、FullScreenDialogFragmentについてのソースコードを
FullScreenDialogFragmentclass FullScreenDialogFragment : BaseFullScreenDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) dialog.setContentView(R.layout.dialog_full_screen) dialog.background_img.setOnClickListener { dialog.background_img.fadeOut(3000) { super.callback!!.onClickScreen() this.dismiss() } } return dialog } // showの後に呼ばれる override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { dialog.background_img.fadeIn(3000) return super.onCreateView(inflater, container, savedInstanceState) } companion object { private var dialogFragment: FullScreenDialogFragment? = null fun newInstance(bundle: Bundle): FullScreenDialogFragment { if (dialogFragment == null) { dialogFragment = FullScreenDialogFragment() } dialogFragment!!.arguments = bundle dialogFragment!!.isCancelable = false return dialogFragment!! } } }onCreateView()のオーバーライドにてfadeIn()をやっています。
これはdialog().show()のあとに呼ばれるメソッドです。このdialog fragmentが継承してるクラスはBaseFullScreenDialogFragmentです。↓
BaseFullScreenDialogFragmentabstract class BaseFullScreenDialogFragment : DialogFragment() { var callback: Callback? = null private val ARG_LISTENER_TYPE = "listenerType" // リスナーのタイプを保持するためのenum private enum class ListenerType { ACTIVITY, FRAGMENT, OTHER } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) return dialog } override fun onAttach(context: Context) { super.onAttach(context) val listenerType = arguments!!.getSerializable(ARG_LISTENER_TYPE) as ListenerType? if (listenerType == ListenerType.ACTIVITY) { this.callback = activity as Callback } else if (listenerType == ListenerType.FRAGMENT) { callback = targetFragment as Callback? } } override fun onStart() { super.onStart() // ダイアログを全画面にする dialog?.window?.apply { setFlags( FLAG_FULLSCREEN, FLAG_LAYOUT_IN_SCREEN ) setBackgroundDrawable(ColorDrawable(Color.WHITE)) setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) } } interface Callback { fun onClickScreen() } fun setOnCallBack(_cbj: Callback) { callback = _cbj // リスナーのタイプを保持 val listenerType: ListenerType? if (this.callback == null) { listenerType = null setTargetFragment(null, 0) } else if (this.callback is Activity) { listenerType = ListenerType.ACTIVITY setTargetFragment(null, 0) } else if (this.callback is Fragment) { listenerType = ListenerType.FRAGMENT setTargetFragment(this.callback as Fragment?, 0) } else { //その他の場合 listenerType = ListenerType.OTHER setTargetFragment(null, 0) } arguments!!.putSerializable(ARG_LISTENER_TYPE, listenerType) } }画面回転するとdialogを呼び元との接続がきれてしまうので呼び出し元を保持する、という方針です。
画面のタップを検知するためにcallbackを設けました。(このタップを検知してfade in, fade outの処理をします)
また、onStart()にて全画面表示にしています。ちなみにxmlは
dialog_full_screen.xml<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > <com.free.myapplication.extension.AnimationImageView android:id="@+id/background_img" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/miri" /> </FrameLayout>とまあこんな感じでやれば実装できます
githubは
https://github.com/takuma0223/tutorial追記: 画面回転でメンバが消えると指摘があったので直しました(家帰ったら点検します)
追記2: 家で確認したところ、バックグラウンドにいったら落ちました。
serializableをCallbackインターフェースでimplementして無理やりbundleに保持したのですが、それがなにやらまずかったようで、調べたら参考になる記事がありました(方針は全く違う)
https://qiita.com/KazaKago/items/999ac7f7392de4657f30