- 投稿日:2020-08-30T21:23:10+09:00
【Android】Retrofitについて
【Android】Retrofitについて
Retrofitとは
Retrofitとは、Http通信をする際に使われるライブラリ。
webApiを叩くとかで、けっこう使うのでは今回は、構成とかは考えず、とりあえず最低限の動くという状態でやってます。
叩くAPIは、QiitaのWebApiです。目次
- build.gradleとManifestへの記述
- Interfaceの作成
- レスポンスのmodel作成
- 実行
1. build.gradleとManifestへの記述
- 依存関係の記述
build.gradle// retrofit implementation "com.squareup.retrofit2:retrofit:2.5.0" // 以下は、gsonというjsonをjavaで扱えるように変換してくれるライブラリ implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
- Manifestへの記述
- 通信を許可するパーミッションを記述
AndroidManifest.xml<uses-permission android:name="android.permission.INTERNET"/>2. Interfaceの作成
ApiService.ktinterface ApiService { // 記事をとってくるメソッドです。 @GET("/api/v2/items") // => GETのリクエスト fun getArticle(): Call<List<Article>> }3. レスポンスのmodel作成
- 先ほど、Interfaceで定義したリクエストに対するレスポンスに対応するmodelクラスを作成します。
Article.ktdata class Article ( val id: String, val title: String, val user: User ) data class User ( val id: String, )4. 実行
- 実行は、Activityで実行してます。
MainActivity.ktclass MainActivity : AppCompatActivity() { val TAG = "MainActivity" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) val retrofit = Retrofit.Builder() .baseUrl("https://qiita.com") .addConverterFactory(GsonConverterFactory.create()) // ここで、jsonをjavaで扱えるようにするGsonを使ってます。 .build() // サービスを実行するクライアントの作成 val articleClient = retrofit.create(ApiService::class.java) // 通信は非同期でやらないといけないので、今回はThreadで実行 Thread().run { // とりあえず、レスポンスは返ってきます。あとは煮るなり焼くなり articleClient.getArticle() } }
- 投稿日:2020-08-30T18:34:55+09:00
Jetpack Composeで頻出する"ambient"とはなにか
普通にアプリの中でも使われる概念で、Jetpack Composeのサンプルアプリのコードや、内部のコードを少しでも読むとすぐに出てくる概念で、知らないと混乱すると思うので、理解しておいたほうが良さそうです。
以下の動画でも登場しました。以下の動画を見てもらってもいいですし、このQiitaでサラッと確認したい人は以下をどうぞ。
https://youtu.be/DDd6IOlH3io?t=445アプリの中で共有したい色などを渡していきたいときはどうすればよいでしょうか? (この場合通常はMaterialThemeを使うことで解決できますが、このように共有したいものはアプリを作る中で出てきます。 参考: https://github.com/android/compose-samples/search?q=ambient&unscoped_q=ambient )
通常Composeのデータフローは上から下に流れていくので、一応、以下のように関数で渡していくことができます。class NewsAppPalette( val primary = ..., val onPrimary = ..., ) @Composable fun NewsApp() { ArticleList( articles = ..., // ** 引数でcolorを渡していっている ** colorPalette = NewsAppPalette( primary = ... onPrimary = ... ), ) } @Composable fun ArticleList(articles: List<Article>, colorPalette: NewsAppPalette) { Row() { articles.forEach { article -> Article(article, colors) } } } fun ArticleItem(article: Article, colorPalette: NewsAppPalette) { Text( text = article.title textColor = colorPalette.onPrimary ) }が、こういうふうに渡していくのはけっこう大変ですよね。。??
Jetpack Composeではこれの代わりにambientを提供します。
サービスロケーターのように名前をつけたオブジェクトを作ってそれを使えるようにします。
これはJetpack ComposeのMaterialTheme
で使われているのと同じ仕組みです。
提供されているComposeのスコープ以下でその変数が利用可能になります。ambientの利用方法
ambientの宣言
ambientでデフォルトで配布されるものをブロックの中に書きます。ambientOf<NewsAppPalette>()
のように 書かなくても宣言できます。val NewsAppColors = ambientOf<NewsAppPalette> { LightColorPalette }ambientの変数の配布。
注意が必要なのが、このブロックの中とそこから呼び出されるComposable functionの中でのみ利用可能になるということです。Prividers(NewsAppColors privides LightColorPalette) { // この中でambientの要素を使える }ambientの変数の使い方
NewsAppColors.currentコードから理解するambientの利用方法
コード的には以下になります。このように引数で渡していかなくても、利用することができています。
val NewsAppColors = ambientOf<NewsAppPalette> { LightColorPalette } @Composable fun NewsApp() { // ** ここでambientをProvideする ** Prividers(NewsAppColors privides LightColorPalette) { ArticleList( // ** 引数でcolorを渡していかなくて良くなっている ** articles = ... ) } } @Composable fun ArticleList(articles: List<Article>) { ... Article(article) ... } fun ArticleItem(article: Article) { Text( text = article.title textColor = NewsAppColors.current.colors.onPrimary ) }(配布されているところの色を変えてわかりやすくしています。)
配布される値の変更
Provideをもう一度することで、そのスコープ内でProvideする値を変えることができます。
(配布されているところの色を変えてわかりやすくしています。)
@Composable fun NewsApp() { // ** ここでambientをProvideする ** Prividers(NewsAppColors privides LightColorPalette) { ArticleList( articles = ... ) } } @Composable fun ArticleList(articles: List<Article>) { ... // ** ここでまた別のDarkColorPaletteをProvideする ** Prividers(NewsAppColors privides DarkColorPalette) { Row() { articles.forEach { article -> Article(article, colors) } } ... } fun ArticleItem(article: Article) { Text( text = article.title textColor = NewsAppColors.current.colors.onPrimary ) }少しだけ効率的に利用する
以下のように
NewsAppColors.current
で利用させるより、そのAmbientへのアクセスをラップオブジェクトを作ってあげるパターンがJetpack Compose内ではよく利用されるようです。object NewsAppTheme { @Composable val colors: NewsAppPalette get() = NewsAppColors.current ... } fun ArticleItem(article: Article) { Text( text = article.title textColor = NewsAppTheme.colors.onPrimary ) }まとめ
これがわかるとかなりの部分のUI周辺のJetpack Composeのコードが読めるようになるので、内部への理解も深まります!
ちょっとはずれますが、 redditではtree-local value
(ツリーローカルな値) という説明になっており、しっくりきます。
- 投稿日:2020-08-30T18:34:55+09:00
Jetpack Composeで頻出する"ambient"とは
普通にアプリの中でも使われる概念で、Jetpack Composeのサンプルアプリのコードや、内部のコードを少しでも読むとすぐに出てくる概念で、知らないと混乱すると思うので、理解しておいたほうが良さそうです。
以下の動画でも登場しました。以下の動画を見てもらってもいいですし、このQiitaでサラッと確認したい人は以下をどうぞ。
https://youtu.be/DDd6IOlH3io?t=445アプリの中で共有したい色などを渡していきたいときはどうすればよいでしょうか? (この場合通常はMaterialThemeを使うことで解決できますが、このように共有したいものはアプリを作る中で出てきます。 参考: https://github.com/android/compose-samples/search?q=ambient&unscoped_q=ambient )
通常Composeのデータフローは上から下に流れていくので、一応、以下のように関数で渡していくことができます。class NewsAppPalette( val primary = ..., val onPrimary = ..., ) @Composable fun NewsApp() { ArticleList( articles = ..., // ** 引数でcolorを渡していっている ** colorPalette = NewsAppPalette( primary = ... onPrimary = ... ), ) } @Composable fun ArticleList(articles: List<Article>, colorPalette: NewsAppPalette) { Row() { articles.forEach { article -> Article(article, colors) } } } fun ArticleItem(article: Article, colorPalette: NewsAppPalette) { Text( text = article.title textColor = colorPalette.onPrimary ) }が、こういうふうに渡していくのはけっこう大変ですよね。。??
Jetpack Composeではこれの代わりにambientを提供します。
サービスロケーターのように名前をつけたオブジェクトを作ってそれを使えるようにします。
これはJetpack ComposeのMaterialTheme
で使われているのと同じ仕組みです。
提供されているComposeのスコープ以下でその変数が利用可能になります。ambientの利用方法
ambientの宣言
ambientでデフォルトで配布されるものをブロックの中に書きます。ambientOf<NewsAppPalette>()
のように 書かなくても宣言できます。val NewsAppColors = ambientOf<NewsAppPalette> { LightColorPalette }ambientの変数の配布。
注意が必要なのが、このブロックの中とそこから呼び出されるComposable functionの中でのみ利用可能になるということです。Prividers(NewsAppColors privides LightColorPalette) { // この中でambientの要素を使える }ambientの変数の使い方
NewsAppColors.currentコードから理解するambientの利用方法
コード的には以下になります。このように引数で渡していかなくても、利用することができています。
val NewsAppColors = ambientOf<NewsAppPalette> { LightColorPalette } @Composable fun NewsApp() { // ** ここでambientをProvideする ** Prividers(NewsAppColors privides LightColorPalette) { ArticleList( // ** 引数でcolorを渡していかなくて良くなっている ** articles = ... ) } } @Composable fun ArticleList(articles: List<Article>) { ... Article(article) ... } fun ArticleItem(article: Article) { Text( text = article.title textColor = NewsAppColors.current.colors.onPrimary ) }(配布されているところの色を変えてわかりやすくしています。)
配布される値の変更
Provideをもう一度することで、そのスコープ内でProvideする値を変えることができます。
(配布されているところの色を変えてわかりやすくしています。)
@Composable fun NewsApp() { // ** ここでambientをProvideする ** Prividers(NewsAppColors privides LightColorPalette) { ArticleList( articles = ... ) } } @Composable fun ArticleList(articles: List<Article>) { ... // ** ここでまた別のDarkColorPaletteをProvideする ** Prividers(NewsAppColors privides DarkColorPalette) { Row() { articles.forEach { article -> Article(article, colors) } } ... } fun ArticleItem(article: Article) { Text( text = article.title textColor = NewsAppColors.current.colors.onPrimary ) }少しだけ効率的に利用する
以下のように
NewsAppColors.current
で利用させるより、そのAmbientへのアクセスをラップオブジェクトを作ってあげるパターンがJetpack Compose内ではよく利用されるようです。object NewsAppTheme { @Composable val colors: NewsAppPalette get() = NewsAppColors.current ... } fun ArticleItem(article: Article) { Text( text = article.title textColor = NewsAppTheme.colors.onPrimary ) }まとめ
これがわかるとかなりの部分のUI周辺のJetpack Composeのコードが読めるようになるので、内部への理解も深まります!
ちょっとはずれますが、 redditではtree-local value
(ツリーローカルな値) という説明になっており、しっくりきます。
- 投稿日:2020-08-30T16:04:12+09:00
初学者によるViewPager2実装方法
はじめに
ViewPager2を実装する際、今までは複数のViewに対してその同数レイアウトファイルを作成していました。
今回は一つのレイアウトファイルで表示できたので紹介します。
ViewPager2実装するのが初めての方にもTabLayoutMediator
とFragmentManager
を理解する助けになればと思います。全体を見たい方向けにサンプルのリポジトリ載せときます。
https://github.com/tomoya-s-101/viewpager2環境
OS: macOS Catalina v10.15.6
IDE: Android Studio v4.0.1導入
TabLayoutを使用できるようにするために、build.gradleにmaterial-componentsライブラリを追加します。
build.gradleimplementation "androidx.viewpager2:viewpager2:1.1.0-alpha01" implementation 'com.google.android.material:material:1.2.0'実装
今回は一つのフラグメントレイアウトで画像の色とテキストの切り替えをViewPager2を使い表示します。
レイアウト
bindingの実装をしたいので、layoutタグで囲います。
tabIndicatorGravityでインジケーターの位置を指定できます。
TabLayoutリファレンスactivity_main.xml<?xml version="1.0" encoding="utf-8"?> <layout> <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=".MainActivity"> <com.google.android.material.tabs.TabLayout android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="48dp" app:tabIndicatorGravity="stretch" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> </com.google.android.material.tabs.TabLayout> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/viewpager2" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toTopOf="@id/tab_layout" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
ViewPager2で表示するレイアウトを作ります。
stringとcolorのリソースも準備してください。fragment_color.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/fragment_color" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tab_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="TabTextView" android:textColor="@color/white" android:textSize="48sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>Fragment
表示させたいリストのインスタンスを生むことで、一つのレイアウトで切り替えが可能になります。
ColorFragment.ktclass ColorFragment: Fragment() { companion object { private const val ARGS_POSITION = "position" fun newInstance (position: Int) = ColorFragment().apply { arguments = Bundle().apply { putInt(ARGS_POSITION, position) } } } private lateinit var binding: FragmentColorBinding private val position: Int by lazy { arguments?.let { it[ARGS_POSITION] as Int }?:0 } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = DataBindingUtil.inflate(inflater, R.layout.fragment_color, container, false) return binding.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) binding.apply { when(position) { 0 -> { tabTextView.setText(R.string.text_red) fragmentColor.setBackgroundResource(R.color.red) } 1 -> { tabTextView.setText(R.string.text_blue) fragmentColor.setBackgroundResource(R.color.blue) } 2 -> { tabTextView.setText(R.string.text_green) fragmentColor.setBackgroundResource(R.color.green) } else -> { tabTextView.setText(R.string.text_yellow) fragmentColor.setBackgroundResource(R.color.yellow) } } } } }Adapter
FragmentActivityを使用するため、
FragmentManager
を呼び出します。
FragmentManagerリファレンスViewPager2Adapter.ktclass ViewPager2Adapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { private val fragments: ArrayList<Fragment> = arrayListOf( ColorFragment.newInstance(0), ColorFragment.newInstance(1), ColorFragment.newInstance(2), ColorFragment.newInstance(3) ) override fun getItemCount(): Int { return fragments.size } override fun createFragment(position: Int): Fragment { return fragments[position] } }Activity
TabLayoutMediator
でTabLaoutとViewPager2を引数で渡すことで、スクロールとインジケーターがリンクします。
TabLayoutMediatorリファレンスMainActivity.ktclass MainActivity : AppCompatActivity() { companion object { fun callingIntent(context: Context) = Intent(context, MainActivity::class.java) } private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main ).apply { viewpager2.adapter = ViewPager2Adapter(supportFragmentManager, lifecycle) TabLayoutMediator(tabLayout, viewpager2) { tab, position -> }.attach() } } }おわりに
他にもViewPager2では、縦方向のスクロールなどアクティブな実装が簡単にできるみたいです。
ドキュメント閲覧ありがとうございました。
2020年7月からエンジニアとして働き始めた見習いですので、拙い点ございます。
間違いやより適切な方法ありましたら、コメントお願いします。
今後も記事を投稿していくので、是非よろしくお願い致します。参考
https://developer.android.com/reference/com/google/android/material/tabs/TabLayout
https://developer.android.com/reference/kotlin/androidx/fragment/app/FragmentManager?hl=ja
https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator
https://developer.android.com/training/animation/vp2-migration?hl=ja
- 投稿日:2020-08-30T15:19:26+09:00
Repositoryから取得したLiveDataをViewModelでキャッシュしておくには?
こうだ
※Developer options の Don't keep activities を有効にしています
SavedStateHandle
AndroidのライフサイクルではActivityインスタンスやFragmentインスタンスはいつ破棄されるかわかりません。
これらが持つViewModelインスタンスも同様です。破棄され、再生成されるとそれまでのメモリー上のデータは破棄されます。これはよろしくありません。
これを避けるためにデータの保存/復元を行う必要があり、そのヘルパークラスであるSavedStateHandleを使うと楽に実装できるというのは以前書かせていただきました。
ViewModelのデータを保存/復元するにはSavedStateHandleを使う
Activityが再生成されると最初から読み込みし直し
上記のケースは、EditTextの入力値のような、データがViewModel内で完結するものでした。
では、データがリポジトリー層から取得される場合はどうすればいいのでしょうか?例を示します。
class User(...): Parcelable() { ... } class UserRepository { fun getUser(): LiveData<User> { ... } } class MainViewModel( private val userRepository: UserRepository ) { val user: LiveData<User> = ? // userRepository.getUser()したいが… }上記のコードは
UserRepository
の戻り値がLiveDataになっています。
SavedStateHandleは初期値にLiveDataを取ることはできませんから、SavedStateHandleは使えません。「何もしない」のも1つの答えです。
通常、リポジトリー層自体が永続化の役割を持っているのですから、あえてViewModelで保存/復元を行わなくてもいいというものです。class MainViewModel( private val userRepository: UserRepository ) { val user: LiveData<User> = userRepository.getUser() // 何も考えずに代入しとけばいいんだよ! }しかし、 LiveDataにデータがemitされるのに時間がかかる場合はどうでしょうか?
class UserRepository { fun getUser(): LiveData<User> { liveData(GlocalContext.coroutineContext + Dispatchers.IO) { // サーバーとの通信やデータ整形などのとても時間のかかる処理 val user: User = veryHeavyProcess() emit(user) } } }このような場合でも、最終的にはデータは表示されます。しかし再度読み込みが行われるため表示が完了するまで時間はかかります。ユーザーからしたらストレスになるでしょう。
// ---------- このへんから追記&修正 ----------
これを解決するためにはどこかでデータをキャッシュしておく必要があります。
リポジトリー層でキャッシュしておくこともできるでしょう。
実際、今回の例もリポジトリー層でキャッシュした方がいい雰囲気のコードです(※後で気づいたが例示として悪かった)。しかし、リポジトリー層で取得したエンティティクラスを画面表示(プレゼンテーション)用にこねくり回している場合、こねくりまわしたデータは既にリポジトリー層の手から離れているためキャッシュ処理はViewModel層が受け持つよりほかありません。
// ---------- このへんまで ----------
したがって、 「画面にはgetUserを使いつつViewModelでgetUserを監視しキャッシュする。ViewModel再生成時はキャッシュした値を初期値として使い、遅延して読み込まれたgetUserの値を使う」 ということをする必要があります。
CachedLiveDataを作ったぞ
上記を解決するためのクラス
CachedLiveData
およびCachedLiveData
を簡単に生成できるSavedStateHandleの拡張関数を作りました。CachedLiveData with SavedStateHandle extension
以下のように使います。
class MainViewModel( private val handle: SavedStateHandle, private val userRepository: UserRepository ) { val user: LiveData<User> = handle.getCachedLiveData<User>("user", userRepository.getUser()) // これだけ }第1引数には
SavedStateHandle#getLiveData
と同じくキー名を指定します。
第2引数には ソースとして使用したいLiveDataを指定します。
これだけで「画面にはgetUserを使いつつViewModelでgetUserを監視しキャッシュする。ViewModel再生成時はキャッシュした値を初期値として使い、遅延して読み込まれたgetUserの値を使う」ことをしてくれます。結果は冒頭に貼った動画のとおりです。
Simple LiveDataが何も使わず単純にuserRepository.getUser
を使った結果、 CachedLiveDataがCachedLiveData
を使った結果です。
両者画面表示が完了したところから動画は始まっています。
ホームボタンを押してランチャー画面を表示させ、アプリアイコンをタップしてアプリを再表示させたところ、Simple LiveDataの方はデータの再読み込みのため表示されるまでに時間がかかるのに対し、CachedLiveDataの方はデータが最初から表示されています。
これはCachedLiveDataによりデータがキャッシュされていたということです。仕組み
CachedLiveData with SavedStateHandle extension
を見ていただければわかるとは思いますが、念のため仕組みの解説をしておきます。
CachedLiveData.ktfun <T : Any> SavedStateHandle.getCachedLiveData(key: String, source: LiveData<T>): LiveData<T> { val cache = getLiveData<T>(key) return CachedLiveData(source, cache) } private class CachedLiveData<T>( source: LiveData<T>, private val cache: MutableLiveData<T> ) : MediatorLiveData<T>() { ... private var isCacheInSource = true ... init { addSource(source) { if (isCacheInSource) { removeSource(cache) isCacheInSource = false ... } value = it cache.value = it } addSource(cache) { value = it } } ... }引数の
source
にuserRepository.getUser
が、cache
にはhandle.getLiveData("user")
が代入されます。
CachedLiveData
は基本的にはsource
の中身が反映され続けます。
しかしsource
の中身が最初に反映されるまでの間はcache
の中身が反映されます。
source
の中身が反映されるようになるとcache
の中身を反映する処理は停止します。そして、source
の中身をcache
の中身として反映する動作を始めます。上記のほか、CachedLiveDataの観測状態と
cache
の観測状態を一致させる処理をつけたら完成です。おわりに
良きViewModelライフを!
- 投稿日:2020-08-30T15:00:40+09:00
最小構成で作るARCore(kotlin)
先日Pixel 4aを買ったのでARCoreを試してみました。
公式のサンプルはJavaで書かれているのですが、サンプルの実装を確認しながら、これをkotlinに直しつつ最小構成でARCoreを動かせないか試してみました。実装したコードはGithubに上げています。
https://github.com/r-asada-ab/SimpleARCore公式サンプル
まず、公式のQuick Startに従ってビルドすれば簡単にARCoreの動作を確認することが出来ます。
https://developers.google.com/ar/develop/java/quickstart
ただ、中で何をしているのかとかどういうクラスが使われているのかとかいろいろ気になりますよね。
ということで、ソースコードを追いながら、それをkotlinで書いてみます。
プロジェクトの作成
新しくAndroid Projectを作ります。
Activityは何でもよいですが、ここではEmpty Activityを選択しておきます。これをビルドすると、普通のAndroidアプリが起動します。
ここからARCoreアプリへと改造していきます。
AndoridManifest
まずAndroid Manifestから。
以下の記述が必要です。
- カメラのパーミッション
- ARCore非対応端末からダウンロードできないようにする
- OpenGLES2.0非対応端末からダウンロードできないようにする
- ARCoreのmeta-dataタグ
以下の記述をAndroidManifestに追加します。
<uses-permission android:name="android.permission.CAMERA"/> <uses-feature android:name="android.hardware.camera.ar" android:required="true"/> <uses-feature android:glEsVersion="0x00020000" android:required="true" /> ... <meta-data android:name="com.google.ar.core" android:value="required" />build.gradle(app)
minSdkVersionを24以上とします。
minSdkVersion 24ARCoreライブラリをdepenedenciesに追加します。
implementation 'com.google.ar:core:1.18.0'実装
これで下準備は終わりです。
具体的な実装に入っていきます。今回ActivtyはMainActivityしかない状態で実装を進めます。
レイアウトの修正
先ず、MainActivityのレイアウトには今TextViewが1つ入っている状態ですが、これをGLSurfaceViewに変えます。
Design画面からは追加できなかったと思うのでコードで以下のように追加します。
親のlayoutは何でも良いですが、今回はLinearLayoutを使いました。<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <android.opengl.GLSurfaceView android:id="@+id/surfaceview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_gravity="top"/> </LinearLayout>Rendererクラスの作成
GLSurfaceViewは描画をGLSurfaceView.Rendererインターフェースを実装したクラスで行うので、そのRenderer実装クラスを作成します。
公式サンプルではMainActivityが実装していましたが、あんまりMainActivityが長くなってもあれなので、新しくクラスを作ります。
import android.opengl.GLES20 import android.opengl.GLSurfaceView.Renderer import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.opengles.GL10 class GLRenderer: Renderer { import android.opengl.GLES20 import android.opengl.GLSurfaceView.Renderer import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.opengles.GL10 class GLRenderer: Renderer { override fun onDrawFrame(unuse: GL10?) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT) GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); } override fun onSurfaceChanged(unuse: GL10?, width: Int, height: Int) { GLES20.glViewport(0, 0, width, height) } override fun onSurfaceCreated(unuse: GL10?, p1: EGLConfig?) { GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); } } }ここで描画のあれこれを行っていきます。
今の段階では特に何も書かなくて良いのですが、動きを確かめるのに一応上のように書いて音きます。これで起動したときに赤で塗りつぶされるのでとりあえず動いているのがわかってよいです。
GLSurfaceViewの初期化
MainActivityのonCreateでGLSurfaceViewの初期化をしていきます。
class MainActivity : AppCompatActivity() { // GLSurfaceView private lateinit var surfaceView: GLSurfaceView // Renderer private lateinit var glRenderer: GLRenderer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) glRenderer = GLRenderer() surfaceView = findViewById(R.id.surfaceview) surfaceView.preserveEGLContextOnPause = true surfaceView.setEGLContextClientVersion(2) surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0) surfaceView.setRenderer(glRenderer) surfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY surfaceView.setWillNotDraw(false) } }これで下のようにひとまず真っ赤な画面が出ていれば問題ありません。
ARCoreの初期化
ここからARCoreを使う準備をしていきます。
先ず最初に次のことを確認します。
- 端末にARCoreがインストールされているか(対応端末には自動でインストールされるので大丈夫かと思いますが)
- カメラのパーミッション
端末にARCoreがインストールされているかは、公式のドキュメントでも太字でImportantと書かれているので確認しておきましょう。
https://developers.google.com/ar/reference/java/arcore/reference/com/google/ar/core/Session
class MainActivity : AppCompatActivity() { private val CAMERA_PERMISSION_CODE = 0 private val CAMERA_PERMISSION = Manifest.permission.CAMERA private var installRequested = false ... override fun onCreate(savedInstanceState: Bundle?) { ... installRequested = false } override fun onResume() { super.onResume() when (ArCoreApk.getInstance().requestInstall(this, !installRequested)) { InstallStatus.INSTALL_REQUESTED -> { installRequested = true return } InstallStatus.INSTALLED -> { } } if (ContextCompat.checkSelfPermission(this, CAMERA_PERMISSION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf<String>(CAMERA_PERMISSION), CAMERA_PERMISSION_CODE) return } } }次いでこれらが大丈夫だった時、Sessionを作成します。
Sessionの作成
Sessionの作成はそこまで複雑でありません。Depthが有効かを見ています(Pixel 4aはまだ対応していていませんがすぐ使えるようになると信じています)。
ただし、このコンストラクタではいくつか例外が投げられる可能性があるので捕捉しておきましょう。
https://developers.google.com/ar/reference/java/arcore/reference/com/google/ar/core/Session
try { session = Session(this) val config: Config = session!!.getConfig() if (session!!.isDepthModeSupported(Config.DepthMode.AUTOMATIC)) { config.depthMode = Config.DepthMode.AUTOMATIC } else { config.depthMode = Config.DepthMode.DISABLED } session!!.configure(config) } catch (e: UnavailableArcoreNotInstalledException) { Log.e(TAG, "Please install ARCore") } catch (e: UnavailableUserDeclinedInstallationException) { Log.e(TAG, "Please install ARCore") } catch (e: UnavailableApkTooOldException) { Log.e(TAG, "Please update ARCore") } catch (e: UnavailableSdkTooOldException) { Log.e(TAG, "Please update this app") } catch (e: UnavailableDeviceNotCompatibleException) { Log.e(TAG, "This device does not support AR") } catch (e: Exception) { Log.e(TAG, "Failed to create AR session") }この状態で起動してもまだ赤いままです。
肝心の描画の部分を書いていきます。カメラ画像の描画
カメラから得られた画像をGLSurfaceViewに書くためのShaderを作成します。
Shaderは公式サンプルだと〇〇RendererクラスがShaderを使ってdrawコールを処理するように設計されていましたが、Shaderの基底クラスを作成して、用途に応じて継承していってもよいかと思います(お好みで)。いつもクラス内定数としてShaderを書いていたのですが、公式サンプルを見るとAssetsにいい感じにまとめてあって便利だったので今回はこちらの方法で記載しました。
頂点シェーダーはこんな感じでテクスチャを1枚ぺらっと張る感じです。
attribute vec4 a_Position; attribute vec2 a_TexCoord; varying vec2 v_TexCoord; void main() { gl_Position = a_Position; v_TexCoord = a_TexCoord; }フラグメントシェーダーも単純ですがこんな感じです。
#extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 v_TexCoord; uniform samplerExternalOES sTexture; void main() { gl_FragColor = texture2D(sTexture, v_TexCoord); }後は良きことこれをロードして、シェーダープログラムを作ってdrawコールします。
Shaderの作成
Shaderは先に記載した文字列を次のようにしてglShaderSourceに渡せばShaderを返してくれます。
fun loadShader(type: Int, shaderCode: String): Int { return GLES20.glCreateShader(type).also { shader -> GLES20.glShaderSource(shader, shaderCode) GLES20.glCompileShader(shader) } }コンパイルのチェックなどが後の記述に増えるのですが、長くなりますのでGithubにコードを上げているのでそちらを参考にされるか、公式のShaderUtilクラスを参考にしてください。
シェーダープログラムの作成
シェーダーが出来たらそれをもとにシェーダープログラムを作ります。
ここでは公式と同じように〇〇Rendererクラスを用意してその中で描画の準備と実際の描画を行うようにします。まず、メンバ変数ですがざっくりとこれくらい必要です。
textureIDやattribute変数のハンドラーを引き回すので、少し多めになります。FLOAT_SIZEとかはハードコーディングでもいいかもですがマジックナンバーを減らすためにも定数化しておくとよいかもです。
class BackgroundRenderer { private val QUAD_COORDS = floatArrayOf( -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f ) private val CAMERA_VERTEX_SHADER_NAME = "shaders/screenquad.vert" private val CAMERA_FRAGMENT_SHADER_NAME = "shaders/screenquad.frag" private val program = 0 private val COORDS_PER_VERTEX = 2 private val TEXCOORDS_PER_VERTEX = 2 private val FLOAT_SIZE = 4 private val quadCoords: FloatBuffer? = null private val quadTexCoords: FloatBuffer? = null private val cameraPositionAttrib = 0 private val cameraTexCoordAttrib = 0 private val cameraTextureUniform = 0 private val cameraTextureId = -1 // コンストラクタ constructor() { } }次にコンストラクタの中で必要な初期化処理を入れていきます。
まずはテクスチャを作成しておきます。
MIN、MAGフィルターなどは目的とする実装に合わせてでよいかと思いますので、今回は公式通りに設定しておきます(たまに思わぬバグの原因になったりしますが。。。)。val textures = IntArray(1) GLES20.glGenTextures(1, textures, 0) cameraTextureId = textures[0] val textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES GLES20.glBindTexture(textureTarget, cameraTextureId) GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR) GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)描画用の座標を準備します。
今回はテクスチャを描画するのでテクスチャ座標も必要です。val numVertices = 4 val bbCoords = ByteBuffer.allocateDirect(QUAD_COORDS.size * FLOAT_SIZE) bbCoords.order(ByteOrder.nativeOrder()) quadCoords = bbCoords.asFloatBuffer().apply { put(QUAD_COORDS) position(0) } val bbTexCoordsTransformed = ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE) bbTexCoordsTransformed.order(ByteOrder.nativeOrder()) quadTexCoords = bbTexCoordsTransformed.asFloatBuffer()最後にシェーダープログラムを作成します。
val vertexShader: Int = Shader.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, CAMERA_VERTEX_SHADER_NAME) val fragmentShader: Int = Shader.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, CAMERA_FRAGMENT_SHADER_NAME) program = GLES20.glCreateProgram() GLES20.glAttachShader(program, vertexShader) GLES20.glAttachShader(program, fragmentShader) GLES20.glLinkProgram(program) GLES20.glUseProgram(program) cameraPositionAttrib = GLES20.glGetAttribLocation(program, "a_Position") cameraTexCoordAttrib = GLES20.glGetAttribLocation(program, "a_TexCoord") Shader.checkGLError(TAG, "Program creation") cameraTextureUniform = GLES20.glGetUniformLocation(program, "sTexture") Shader.checkGLError(TAG, "Program parameters")描画部分の実装
これで準備が出来たので描画部分を書いていきます。
GLRendererのonDrawFrameが呼ばれたときに行う描画処理を書いていきます。public fun draw(frame: Frame) { if (frame.hasDisplayGeometryChanged()) { frame.transformCoordinates2d( Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES, quadCoords, Coordinates2d.TEXTURE_NORMALIZED, quadTexCoords ) } if (frame.timestamp == 0L) { return } quadTexCoords!!.position(0) GLES20.glActiveTexture(GLES20.GL_TEXTURE0) GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId) GLES20.glUseProgram(program) GLES20.glUniform1i(cameraTextureUniform, 0) GLES20.glVertexAttribPointer( cameraPositionAttrib, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadCoords ) GLES20.glVertexAttribPointer( cameraTexCoordAttrib, TEXCOORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadTexCoords ) GLES20.glEnableVertexAttribArray(cameraPositionAttrib) GLES20.glEnableVertexAttribArray(cameraTexCoordAttrib) GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) GLES20.glDisableVertexAttribArray(cameraPositionAttrib) GLES20.glDisableVertexAttribArray(cameraTexCoordAttrib) }これをGLRender.onDrawFrameから呼び出すように書いて仕上げとなります。
override fun onDrawFrame(unuse: GL10?) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT) if (session == null) { return } if (viewportChanged) { val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager val display = windowManager.defaultDisplay val displayRotation = display.getRotation() session!!.setDisplayGeometry(displayRotation, viewportWidth, viewportHeight) viewportChanged = false } session!!.setCameraTextureName(backgroundRenderer.getTextureId()) val frame = session!!.update() backgroundRenderer.draw(frame) } override fun onSurfaceChanged(unuse: GL10?, width: Int, height: Int) { GLES20.glViewport(0, 0, width, height) viewportWidth = width viewportHeight = height viewportChanged = true }session.setDisplayGeometry()ですが、onSurfaceChangedで幅や高さが変更になったとき(初めてonSurfaceChangedが呼ばれる時も含めて)必須で必要になります。
以上で、ARCoreを利用した最小構成のアプリができるかと思います。
アプリを起動するとカメラの映像が反映されているのを確認できます。さらにARCoreで検出した平面を描画したいとか、特徴点を点で描画したいとかであれば、同様にしてシェーダーを追加で書いていくとよいかと思います。
まとめ
最初構成と言いながら結構ありますがまとめると次の設定をしておけばよいですね。
- AndroidManifestの設定
- カメラのパーミッション
- ARCore非対応端末からダウンロードできないようにする
- OpenGLES2.0非対応端末からダウンロードできないようにする
- ARCoreのmeta-dataタグ
- build.gradle(app)の設定
- minSdkVersionを24以上にする
- ARCoreライブラリを追加する
- GLSurfaceViewの実装
- GLSurfaceViewをレイアウトファイルに追加する
- GLSurfaceView.Rendererインターフェースを実装したクラスを作る
- GLSurfaceViewを初期化する
- ARCoreの初期化
- ARCoreが端末にインストールされているか確認する
- カメラのパーミッションを確認する
- Sessionを初期化する
- Shaderの作成
- フラグメントシェーダーを書く
- 頂点シェーダーを書く
- シェーダーをロードする
- シェーダープログラムを作る
- シェーダーで描画する
- 投稿日:2020-08-30T13:43:09+09:00
ViewPagerにある異なるFragment上のListViewのスクロールを同期させる
実現すること
タイトルの通り、
ViewPager
のページをめくったときに遷移先のページと元のページのListView
のスクロール量が同期するようにします。
時刻表を作るときなどに活用できます。前提
以下のような環境をもとに、実装を進めていきます。
表示するリスト
Data.ktobject Data { val list = (1..100).toList() }
Avtivity
の実装MainActivity.ktclass MainActivity : AppCompatActivity() { lateinit var viewPager: ViewPager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewPager = findViewById(R.id.view_pager) viewPager.apply { adapter = ViewPagerAdapter(supportFragmentManager, viewPager) } } }
MainActivity
のレイアウトactivity_main.xml<?xml version="1.0" encoding="utf-8"?> <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=".MainActivity"> <androidx.viewpager.widget.ViewPager android:layout_height="match_parent" android:layout_width="match_parent" android:id="@+id/view_pager"/> </androidx.constraintlayout.widget.ConstraintLayout>
ViewPager
のアダプターViewPagerAdapter.ktclass ViewPagerAdapter(fragmentManager: FragmentManager, private val container: ViewPager) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT){ override fun getCount(): Int { return 5 //ページ数:5 } override fun getItem(position: Int): Fragment { return PageFragment.newInstance(position).apply { adapter = this@ViewPagerAdapter } } }
ViewPager
に充てるFragment
の実装kotlinPageFragment.ktclass PageFragment : Fragment() { //自身が何ページ目か private var page = 0 private lateinit var listView: ListView //自身が属すViewPager private var viewPagerAdapter: ViewPagerAdapter? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { page = it.getInt(ARG_PAGE) } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_page, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { listView = requireView().findViewById(R.id.list_view) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) listView.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, Data.list) } companion object { const val ARG_PAGE = "Page" @JvmStatic fun newInstance(page: Int) = PageFragment().apply { arguments = Bundle().apply { putInt(ARG_PAGE, page) } } } }
PageFragment
のレイアウトfragment_page.xml<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".PageFragment"> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/list_view" /> </FrameLayout>処理の流れ
listView
がスクロールを感知するviewPagerAdapter
に通知viewPagerAdapter
が左右1ページずつのPageFragment
に通知- スクロールを更新する
注意点
OnScrollListener
はユーザーがスワイプする前(生成時など)にも反応してしまうため、ユーザーがlistView
を操作できるようになって初めて反応するように設定しないと、ページを繰るたびにスクロール量が0になってしまいます。- ページをめくって、そこで
listView
をスワイプせずにまたページをめくるとOnScrollListener
が起動しないまま次のページに進んでしまい、次のページのlistView
が初期位置のままになってしまいます。以上2点を解決できるように実装しないといけません。
要点
なぜ
viewPagerAdapter
を経由して通知するのか
viewPagerAdapter
を通さないと、隣のフラグメントを取得できないからです。
具体的にはviewPagerAdapter
内でinstantiateItem(container: ViewGroup, position: Int)
を呼び出すことで取得できます。container
はこのアダプタが属すViewPager
を渡せばよいです。あるフラグメントにページが遷移したことを知る方法
ViewPagerAdapter
の振る舞いをBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
として設定しているので、そのフラグメントにページが遷移したときにのみonResume
が実行されます。そのため、lifecycle.currentState == Lifecycle.State.RESUMED
のとき、自身のページが表示されていること、Lifecycle.State.STARTED
のとき今表示されているページの隣にあることが分かります。実装
上で提示した処理の流れの逆順に実装していきます。
PageFragment
がviewPagerAdapter
から通知を受け、スクロールを更新するメソッド
PageFragment
のライフサイクル状態がSTARTED
のとき、つまりView
が作成されてからViewPager
上でこのフラグメントにページが変わるまでの間、スクロールの更新を受け付けます。PageFragmentfun updateScroll(position: Int, y: Int){ if(lifecycle.currentState == Lifecycle.State.STARTED){ listView.setSelectionFromTop(position, y) } }
ViewPagerAdapter
がlistView
から通知を受け、左右1ページずつのPageFragment
に更新を通知するメソッドViewPagerAdapterfun updateScroll(page: Int, listPosition: Int, listY: Int){ var previous: PageFragment? = null if(page > 0) previous = instantiateItem(container, page - 1) as PageFragment var next: PageFragment? = null if(page < this.count - 1) next = instantiateItem(container, page + 1) as PageFragment previous?.updateScroll(listPosition, listY) next?.updateScroll(listPosition, listY) }
listView
がスクロールを感知し、ViewPagerAdapter
に通知する部分ライフサイクル状態が
RESUMED
であること、つまり、このフラグメントが表示されていてユーザーが操作可能であることを確認し、1つ目の注意点を解決しました。PageFragment.onActivityCreatedlistView.setOnScrollListener(object: AbsListView.OnScrollListener{ override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) { } override fun onScroll(view: AbsListView?, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) { if(lifecycle.currentState == Lifecycle.State.RESUMED){ viewPagerAdapter?.updateScroll(page, listView.firstVisiblePosition, listView.getChildAt(0).top) } } })2つ目の問題点は
PageFragment
に以下のようなonResume
を実装することで解決できます。
ページが自分に遷移した瞬間に両隣のスクロール量を自らに合わせるという処理をしています。PageFragmentoverride fun onResume() { super.onResume() viewPagerAdapter?.updateScroll(page, listView.firstVisiblePosition, listView.getChildAt(0)?.top ?: 0) }おわりに
この投稿は初投稿です。
これを解決するのに丸一日掛かったので、備忘録として残しておこうと思いました。
- 投稿日:2020-08-30T13:08:18+09:00
ESP-WROOM-02(ESP8266)の基本回路
毎回、ブレッドボードを組むときに構成を調べないで済むようにするための忘備録です。
接続
説明
PIN 説明 3V3 電源のプラスライン(+)に接続 GND 電源のマイナスライン(-)に接続 EN プルアップ抵抗(10KΩ)を通してプラスライン(+)に接続 IO0 プルアップ抵抗(10KΩ)を通してプラスライン(+)に接続
併せてFLASHスイッチを経由してマイナスラインにも接続IO2 プルアップ抵抗(10KΩ)を通してプラスライン(+)に接続 IO15 プルダウン抵抗(10KΩ)を通してマイナスライン(-)に接続 RST RESETスイッチを経由してマイナスライン(-)に接続 TXD UARTコントローラのRXDと接続 RXD UARTコントローラのTXDと接続 回路図
RESETスイッチの使い方
本稿のRESETスイッチはPUSHで通電するPUSH型のタクトスイッチを想定している。
モード 操作 プログラム実行モード FLASHスイッチをOFF(押さない)状態でRESETスイッチをON(押す)/OFF(離す)する。
RESETスイッチ離した瞬間にリセットがかかり、プログラムが実行される。プログラム書込モード FLASHスイッチをON(押す)状態でRESETスイッチをON(押す)/OFF(離す)する。
FLASHスイッチがONのままRESETスイッチ離した瞬間にプログラム書き込みモードでリセットがかかる。
プログラムはシリアルインタフェイス(UART)経由で流し込む
- 投稿日:2020-08-30T09:42:41+09:00
【Android 9.0 Pie Java】RecyclerView 長押しで要素毎に内容の変わるコンテキストメニューを表示する
RecyclerViewで要素を長押しして、要素毎に内容の変わるコンテキストメニューを表示する方法の記事が見当たらなかったのでメモしてみます!
実装方法
コンテキストメニューの表示
Activity or Fragmentに
View.OnCreateContextMenuListenerをimplementsします。
SampleFragment.javapublic class SampleFragment extends Fragment implements View.OnCreateContextMenuListener {他にもViewHolderにimplementsするパターンもありますが、
コンテキストメニューを要素毎に内容を出し分けしたくて、都合のよかったFragmentにimplementsしました。次に、RecyclerViewを生成をしているonCreatedViewメソッドで
SampleFragment.javaRecyclerView rv = view.findViewById(R.id.recyclerView); // Adapter等のアタッチは省略します registerForContextMenu(rv); // 追記を追記します。
registerForContextMenu
はView.OnCreateContextMenuListenerをimplementsすることで呼び出せるようになります。
この記述でコンテキストメニューを認識させることができます。続いて、Fragmentに
onCreateContextMenu
を実装します。SampleFragment.java@Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { // 長押しされたメッセージ位置を取得し、 // RecyclerViewで表示させているListのindexとして扱い、 // ユーザーIDを取得してメニューの出し分けを実現 if (list.get(adapter.getPosition()).getUserId() == loginUserId) { menu.add(Menu.NONE, 1, Menu.NONE, "コピー"); menu.add(Menu.NONE, 2, Menu.NONE, "編集"); menu.add(Menu.NONE, 3, Menu.NONE, "削除"); } else { menu.add(Menu.NONE, 1, Menu.NONE, "コピー"); } }ContextMenuオブジェクトに追加したい内容をaddする形で簡単に項目を追加できます。
addメソッドの第二引数に数値をセットしていますが、これは押された際のリスナーのハンドリングで使用します。
参考程度ですが、Adapterに以下の要領でgetPosition()
メソッドを用意して、長押しされた要素の位置を取得できるようにしています。Adapter.java@Override public void onBindViewHolder(final ChatViewHolder holder, int position) { // 省略 holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { setPosition(holder.getPosition()); return false; } }); } // 長押しされた要素の位置を取得する public int getPosition() { return position; } // 長押しされた要素の位置をセットする public void setPosition(int position) { this.position = position; }ここまでの実装でコンテキストメニューは表示できます。
コンテキストメニューのイベントリスナー
以下のように、
onContextItemSelected
を実装し、中でswitch文を用いてハンドリングします。
MenuItem.getItemId()で先ほど登録した各要素の判別用の数値が取得できますので、
switch文で振り分ける感じですね。SampleFragment.java@Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case 1: // TODO メッセージをクリップボードにコピー return true; case 2: // TODO メッセージを編集 return true; case 3: // TODO メッセージを削除 return true; default: return false; } }以上です。
どなたかの参考になりましたら幸いです!!
- 投稿日:2020-08-30T09:06:41+09:00
adb connectでmissing port in specificationとなる場合
はじめに
adbのバージョンによるのか、adb connnectで接続できない場合があったため備忘録
コマンド
C:\Users\hoge>adb tcpip 5555 restarting in TCP mode port: 5555 rem 接続に失敗する C:\Users\hoge>adb connect 192.168.XXX.XXX missing port in specification: tcp:192.168.XXX.XXX rem ポート番号を末尾に追加すると接続できる C:\Users\hoge>adb connect 192.168.XXX.XXX:5555 connected to 192.168.XXX.XXX:5555 C:\Users\hoge>adb devices List of devices attached 192.168.XXX.XXX:5555 device確認したバージョン
C:\Users\hoge>adb version Android Debug Bridge version 1.0.41 Version 29.0.1-5644136参考サイト
- 投稿日:2020-08-30T01:34:02+09:00
ViewModelのデータを保存/復元するにはSavedStateHandleを使う
こうだ
※
Developer options
のDon't keep activities
を有効にしていますAndroidとメモリー上のデータ
Androidはすぐメモリー上のデータが飛びます。
画面を回転したら飛ぶ、システムがアクティビティを破棄したら飛ぶ、システムがプロセスをキルったら飛ぶ。これらはAndroidのライフサイクルのせいで、上記のタイミングでActivityやFragmentのインスタンスが都度作り直しになるためです。
そのために開発者はActivityやFragmentのonSaveInstanceState
関数で飛ばしたくないデータを保存し、onCreate
関数などでsavedInstanceState
からデータを復元する処理を書かなくてはいけませんでした。ViewModelの登場
AndroidでViewModelクラスが登場して以降、開発者は様々な恩恵を受けられるようになりました。
その一つにメモリー上のデータが飛びにくくなったことがあります。
ViewModelは画面を回転させてもデータは飛びません! 自動で復元されます!
やったね!……?ViewModelの問題
しかしViewModelといえど、システムがアクティビティを破棄した時、システムがプロセスをキルした時はやはり飛びます。No, やったね!。やってない。
結局、ViewModle内の状態も
savedInstanceState
に都度保存・から都度復元しなければならないのでしょうか?SavedStateHandle
いいえ、そんなことはありません。ViewModelには
SavedStateHandle
という便利なものが用意されています。
SavedStateHandle
はViewModel内のプロパティの保存/復元を行うためのヘルパークラスです。
SavedStateHandle
の内部でプロパティの実体を保持しており、ライフサイクルを勝手に観測していい感じに保存・復元をしてくれます。ここで例を見てみましょう。
保存/復元を行いたいのはEditText
とデータバインディングされたval userTextInput: MutableLiveData<String>
プロパティであるとします。
保存/復元を考えない場合、userTextInput
はval userTextInput = MutableLiveData<String>("")
のように宣言されていたことでしょう。
これをSavedStateHandleを使って保存/復元するには以下のように書きます。MainFragment.ktclass MainFragment : Fragment() { ... // SavedStateHandleを使っても使わなくてもViewModelの初期化方法は同じ。 private val viewModel: MainViewModel by viewModels() ... }MainViewModel.ktclass MainViewModel( // ViewModelの第1引数にSavedStateHandleを指定します handle: SavedStateHandle ) : ViewModel() { // SavedStateHandleからキー"USER_TEXT_INPUT"にひも付くMutableLiveDataを探して引っ張ってくる。 // 存在しない場合は新しく作る(そしてSavedStateHandle内でキー"USER_TEXT_INPUT"に対してひも付け、抱えておく)。 // 初期値は空文字。 val userTextInput: MutableLiveData<String> = handle.getLiveData<String>("USER_TEXT_INPUT", "")普通にMutableLiveDataのコンストラクタから宣言していたのを
handle
の関数から取得するようにしただけです。
これだけでシステムがアクティビティを破棄する時、システムがプロセスをキルする時はその時のuserTextInputの値を勝手に保存してくれますし、復帰する時は勝手に復元してくれます。
val userTextInput = MutableLiveData<String>("")
(Simple LiveData)方式とval userTextInput: MutableLiveData<String> = handle.getLiveData<String>("USER_TEXT_INPUT", "")
(SavedStateHandle#getLiveData)方式の振る舞いを比較したのが冒頭の動画になります。
EditText
に文字を入力したあとホームボタンを押してランチャー画面を表示させ、再度アプリアイコンをタップしてアプリを起動させたところ、Simple LiveDataの方は入力したデータが飛んでしまっていますが、SavedStateHandle#getLiveDataの方はきちんとデータが残っています。これは保存/復元が行われたということです。今度こそやったね!なお、
handle.getLiveData<T>(...)
のT
になれるのは、
- 基本型
- Boolean
- Short
- Int
- Long
- Float
- Double
- String
- Byte
- Char
- CharSequence
- Parcelable
- Array<基本型 または Parcelable>
- Serializable
- Binder
- Bundle
- Size
- SizeF
- ArrayList<
T
になれるどれか>- SparseArray<
T
になれるどれか>のみです。
T
がエンティティクラスの場合はParcelableを継承し実装するのがよいでしょう。ちなみに
SavedStateHandle
はget
という関数も持っています。
これはMutableLiveData<T>
ではなく生の値(T
)を返す関数です。
使う機会は多くないかもしれませんが覚えておくといいでしょう。おわりに
Androidのライフサイクルに泣きたくなるのをこらえて頑張りましょう!
追記
続きのような立ち位置の記事を書きました。合わせてどうぞ。
- 投稿日:2020-08-30T00:30:22+09:00
ImageViewを角丸にする方法
概要
Androidには、画像リソースを表示するためにImageViewクラスが存在します。これを使用することで、UIに画像を表示することが可能になります。しかしながら、下の画像のように丸いアイコンのような画像を表示させる属性は、存在しません。(2020/08/29現在)
なので今回は、ImageViewを角丸にする方法を紹介します。サードパーティのライブラリも複数存在しますが、今回はそのよなライブラリに頼らずAndroid標準のライブラリのみ使用して実装する方法を紹介します。
実装方法
実装のコードは下記のようになります。
ポイントはCardViewを使用することです。<androidx.cardview.widget.CardView android:id="@+id/icon" android:layout_width="40dp" android:layout_height="40dp" app:cardBackgroundColor="@color/colorAccent" app:cardCornerRadius="20dp" app:cardElevation="0dp" <ImageView android:id="@+id/image_view" android:layout_height="wrap_content" android:layout_width="wrap_content" android:src="@drawable/test_jpg" android:scaleType="centerCrop" android:layout_gravity="center" /> </androidx.cardview.widget.CardView>ざっくり実装方針を説明すると、CardViewの子としてImageViewを配置し、CardViewで角丸を作成しています。
CardViewには、
app:cardCornerRadius
という属性が存在し、角の半径を指定することができます。この属性により角丸が実現できます。まとめ
今回はImageViewを角丸にする方法を紹介しました。
サードパーティのライブラリを使用しなくても、簡単に実装できますので、ぜひ使用してみてください。