20210503のAndroidに関する記事は11件です。

TabbedActivityのサンプルプロジェクトをViewPager2化する

TabLayout+ViewPagerな画面を作る場合、新規ActivityのGaralyにあるTabbedActivityが参考になります。 ただ、Android Studio 4.1.3では、タブのフォアグラウンドとバックグラウンドが同じ色になっていて、テキストもインジケータも見えないとかちょっとおかしなことになっています。 タイトル部分もToolbarと思いきやTextViewですね。どうしてでしょ? ViewPager2にする話をしたいのですが、その前にちょっと手直ししておきます。 ViewPagerサンプルとしての手直し レイアウトはTextViewをToolbarに置き換えておきます(FloatingActionButtonは本題から外れるので外しています) activity_view_pager.xml <?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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=".a.ViewPagerActivity" > <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/Theme.MyApplication.AppBarOverlay" > <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/Theme.MyApplication.PopupOverlay" /> <com.google.android.material.tabs.TabLayout android:id="@+id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.google.android.material.appbar.AppBarLayout> <androidx.viewpager.widget.ViewPager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> 配色の問題についてはこれだけで解決します。 ActivityについてはViewBindingを使うように修正していますがほぼ変更はありません。 ViewPagerActivity.kt class ViewPagerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityViewPagerBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) val sectionsPagerAdapter = SectionsPagerAdapter(this, supportFragmentManager) binding.viewPager.adapter = sectionsPagerAdapter binding.tabs.setupWithViewPager(binding.viewPager) } } ViewModelについてはそのままにしています。 PageViewModel.kt class PageViewModel : ViewModel() { private val _index = MutableLiveData<Int>() val text: LiveData<String> = Transformations.map(_index) { "Hello world from section: $it" } fun setIndex(index: Int) { _index.value = index } } PagerAdapterについては結構大きく変えました。 FragmentPagerAdapterはFragmentManagerだけのコンストラクタはDeprecatedのため、新しいbehaviorを指定するコンストラクタを使い、BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENTを指定します。behaviorを省略した場合BEHAVIOR_SET_USER_VISIBLE_HINTを指定したのと同じ挙動になりますが、こちらの動作自体がDeprecatedです。 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENTを指定すると、名前の通り、ユーザーに見えているFragmentだけがResume状態となるように動作します。旧来の動作では、表示されていないFragmentもResume状態となり、ユーザに見えているかどうかはsetUserVisibleHintで検出していました。この動作はDeprecatedなので今後は使わないようにしましょう。 なお、ViewPager2の動作もBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENTと同様です。 コンストラクタではViewModelの初期設定を行っています。サンプルではなぜかPageViewModelをFragmentスコープで持ち、Fragmentで初期化するということをやっています。これだと、FragmentがリサイクルされたときにViewModelも失われるので表示状態を保持することができません。ここにあるようにActivityスコープでページのポジションをキーとして保持するようにする必要があります。 SectionsPagerAdapter.kt private val TAB_TITLES = arrayOf( R.string.tab_text_1, R.string.tab_text_2 ) class SectionsPagerAdapter( private val activity: FragmentActivity, fm: FragmentManager ) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { init { repeat(count) { activity.pageViewModels<PageViewModel>({ it }).value.setIndex(it + 1) } } override fun getItem(position: Int): Fragment = PlaceholderFragment.newInstance(position) override fun getPageTitle(position: Int): CharSequence = activity.resources.getString(TAB_TITLES[position]) override fun getCount(): Int = 2 } pageViewModelsはこういう定義になっています。keyedViewModelsについては「keyを指定したViewModelの取得もlazy拡張関数で簡単にしたい」をご参考 ViewModelExtensions.kt @MainThread inline fun <reified VM : ViewModel> ComponentActivity.pageViewModels( noinline pageIndex: () -> Int, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> = keyedViewModels({ pageIndex().toString() }, factoryProducer) @MainThread inline fun <reified VM : ViewModel> Fragment.pageViewModels( noinline pageIndex: () -> Int, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> = keyedActivityViewModels({ pageIndex().toString() }, factoryProducer) 最後にFragmentです。コンストラクタでレイアウトを指定し、onCreateやonCreateViewはoverrideしません。 代わりにonViewCreatedでラベル設定を行います。 サンプルのままでは、FragmentでLiveDataをobserveするときはlifecycleOwnerはviewLifecycleOwnerを使えと怒られるので怒られないように修正。 PlaceholderFragment.kt class PlaceholderFragment : Fragment(R.layout.fragment_view_pager) { private val pageViewModel: PageViewModel by pageViewModels({ requireArguments().getInt(KEY_POSITION) }) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = FragmentViewPagerBinding.bind(view) pageViewModel.text.observe(viewLifecycleOwner) { binding.sectionLabel.text = it } } companion object { private const val KEY_POSITION = "ARG_POSITION" @JvmStatic fun newInstance(sectionNumber: Int): PlaceholderFragment = PlaceholderFragment().apply { arguments = bundleOf(KEY_POSITION to sectionNumber) } } } ViewPager2化する ここから、前項でViewPagerで実装していた部分をViewPager2に置き換えていきます。 同じ構成にするならViewModel/Fragmentは変更する必要はありません。 PagerAdapterはFragmentPagerAdapterもしくはFragmentStatePagerAdapterを使っているところを、FragmentStateAdapterに置き換えます。 getItemの代わりにcreateFragment、getCountの代わりにgetItemCountを実装します。 TabLayoutにタイトルを伝えるgetPageTitleに相当するメソッドはありません。この処理はActivityの方に移動させています。 SectionsPagerAdapter.kt class SectionsPagerAdapter( activity: FragmentActivity ) : FragmentStateAdapter(activity) { init { repeat(itemCount) { activity.pageViewModels<PageViewModel>({ it }).value.setIndex(it + 1) } } override fun getItemCount(): Int = 2 override fun createFragment(position: Int): Fragment = PlaceholderFragment.newInstance(position) } レイアウトでは、ViewPagerのところをViewPager2に置き換えます activity_view_pager.xml - <androidx.viewpager.widget.ViewPager + <androidx.viewpager2.widget.ViewPager2 AcitivtyではTabLayoutとViewPagerの接続部分を変更します。 setupWithViewPagerは使えないので、TabLayoutMediatorを使って実装します。 こちらはtabへの反映処理を実装できるので、単にテキストを変更する以上の操作も可能ですね。 ViewPagerActivity.kt private val TAB_TITLES = arrayOf( R.string.tab_text_1, R.string.tab_text_2 ) class ViewPagerActivity : AppCompatActivity() { private lateinit var binding: ActivityViewPager2Binding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityViewPager2Binding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) val sectionsPagerAdapter = SectionsPagerAdapter(this) binding.viewPager.adapter = sectionsPagerAdapter TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position -> tab.text = getString(TAB_TITLES[position]) }.attach() } } 以上が、ViewPager2に対応したTabbedActivityのサンプルになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PWAにアプリショートカットを導入したらほぼネイティブアプリになった。

こんばんは。ちょっとしたTipsでござる。 今回はPWAを更にネイティブアプリにすべくアプリショートカットを導入しましょう。 App-Shortcutとはアプリを長押ししてら出てくるやつのことです。 ↓こんなやつ アプリショートカットはChrome84からPWAでも使えるようになりました。 超簡単にユーザーの生産性向上につながると思うので実装したほうが良いかも知れませんってことでやりましょう。 今回は私のブログで導入してみました。 事前事項 詳細な仕様などは公式サイトを見てください。 ※PWA系の記事で毎回書いてるんですけどSSL対応が必須です。テストでやりたいならGitHub Pagesとかを使うといいかも知れません。 対応状況 対応しているプラットフォームは以下のとおりです。(2021/05/03現在) プラットフォーム 是非 Windows ○(Chrome85およびEdge85) MacOS × Android ○(Chrome85) iOS × ChromeOS △(記事下部参照) Apple系がまだ非対応ですね。 導入方法 導入はとてもかんたんでmanifest.jsonにshortcuts配列を追加するだけです。ServiceWorkerの変更とかも不要です。 manifest.json { "name": "Player FM", "start_url": "https://hoge.com", "shortcuts": [ { "name": "Open Play Later", "short_name": "Play Later", "description": "View the list of podcasts you saved for later", "url": "/play-later", "icons": [{ "src": "/icons/play-later.png", "sizes": "192x192" }] }, { "name": "View Subscriptions", "short_name": "Subscriptions", "description": "View the list of podcasts you listen to", "url": "/subscriptions", "icons": [{ "src": "/icons/subscriptions.png", "sizes": "192x192" }] } ] } 名前とURLだけは必須です。アイコン等はアプションですが設定しとくといいでしょう。 ※アイコンには現時点ではSVGファイルはサポートされていません。代わりにPNGを使用してください。 動作確認 ブラウザで確認 ちゃんと反映されてるか確認したい場合、ブラウザの開発者モードのApplicationタブでチェックで来ます。 Android Androidではネイティブアプリと同様に長押しで表示できます。設定したアイコンと名前、リンク先が反映されています。 Windows Windowsの場合はスタートメニューを右クリックもしくはタスクバーのアイコンを右クリックで表示できます。 MacOS Macはやっぱ非対応でした。 ChromeOS ChromeOSではChrome 92以降の実験機能で利用できるそうです。 chrome://flags/#enable-desktop-pwas-app-icon-shortcuts-menu-uiをONにするだけでできます。 公式より↓ かんたんなので皆さんも是非お試しあれ。 【ブログ】https://0115765.com/ 【Twitter】@tomox0115
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PWAにアプリショートカットを導入する

こんばんは。ちょっとしたTipsでござる。 今回はPWAを更にネイティブアプリにすべくアプリショートカットを導入しましょう。 App-Shortcutとはアプリを長押ししてら出てくるやつのことです。 ↓こんなやつ アプリショートカットはChrome84からPWAでも使えるようになりました。 超簡単にユーザーの生産性向上につながると思うので実装したほうが良いかも知れませんってことでやりましょう。 今回は私のブログで導入してみました。 事前事項 詳細な仕様などは公式サイトを見てください。 ※PWA系の記事で毎回書いてるんですけどSSL対応が必須です。テストでやりたいならGitHub Pagesとかを使うといいかも知れません。 対応状況 対応しているプラットフォームは以下のとおりです。(2021/05/03現在) プラットフォーム 是非 Windows ○(Chrome85およびEdge85) MacOS × Android ○(Chrome85) iOS × ChromeOS △(記事下部参照) Apple系がまだ非対応ですね。 導入方法 導入はとてもかんたんでmanifest.jsonにshortcuts配列を追加するだけです。ServiceWorkerの変更とかも不要です。 manifest.json { "name": "Player FM", "start_url": "https://hoge.com", "shortcuts": [ { "name": "Open Play Later", "short_name": "Play Later", "description": "View the list of podcasts you saved for later", "url": "/play-later", "icons": [{ "src": "/icons/play-later.png", "sizes": "192x192" }] }, { "name": "View Subscriptions", "short_name": "Subscriptions", "description": "View the list of podcasts you listen to", "url": "/subscriptions", "icons": [{ "src": "/icons/subscriptions.png", "sizes": "192x192" }] } ] } 名前とURLだけは必須です。アイコン等はアプションですが設定しとくといいでしょう。 ※アイコンには現時点ではSVGファイルはサポートされていません。代わりにPNGを使用してください。 動作確認 ブラウザで確認 ちゃんと反映されてるか確認したい場合、ブラウザの開発者モードのApplicationタブでチェックで来ます。 Android Androidではネイティブアプリと同様に長押しで表示できます。設定したアイコンと名前、リンク先が反映されています。 Windows Windowsの場合はスタートメニューを右クリックもしくはタスクバーのアイコンを右クリックで表示できます。 MacOS Macはやっぱ非対応でした。 ChromeOS ChromeOSではChrome 92以降の実験機能で利用できるそうです。 chrome://flags/#enable-desktop-pwas-app-icon-shortcuts-menu-uiをONにするだけでできます。 公式より↓ かんたんなので皆さんも是非お試しあれ。 【ブログ】https://0115765.com/ 【Twitter】@tomox0115
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PWAにアプリショートカットを導入する(長押しオプション)

こんばんは。ちょっとしたTipsでござる。 今回はPWAを更にネイティブアプリにすべくアプリショートカットを導入しましょう。 App-Shortcutとはアプリを長押ししてら出てくるやつのことです。 ↓こんなやつ アプリショートカットはChrome84からPWAでも使えるようになりました。 超簡単にユーザーの生産性向上につながると思うので実装したほうが良いかも知れませんってことでやりましょう。 今回は私のブログで導入してみました。 事前事項 詳細な仕様などは公式サイトを見てください。 ※PWA系の記事で毎回書いてるんですけどSSL対応が必須です。テストでやりたいならGitHub Pagesとかを使うといいかも知れません。 対応状況 対応しているプラットフォームは以下のとおりです。(2021/05/03現在) プラットフォーム 是非 Windows ○(Chrome85およびEdge85) MacOS × Android ○(Chrome85) iOS × ChromeOS △(記事下部参照) Apple系がまだ非対応ですね。 導入方法 導入はとてもかんたんでmanifest.jsonにshortcuts配列を追加するだけです。ServiceWorkerの変更とかも不要です。 manifest.json { "name": "Player FM", "start_url": "https://hoge.com", "shortcuts": [ { "name": "Open Play Later", "short_name": "Play Later", "description": "View the list of podcasts you saved for later", "url": "/play-later", "icons": [{ "src": "/icons/play-later.png", "sizes": "192x192" }] }, { "name": "View Subscriptions", "short_name": "Subscriptions", "description": "View the list of podcasts you listen to", "url": "/subscriptions", "icons": [{ "src": "/icons/subscriptions.png", "sizes": "192x192" }] } ] } 名前とURLだけは必須です。アイコン等はアプションですが設定しとくといいでしょう。 ※アイコンには現時点ではSVGファイルはサポートされていません。代わりにPNGを使用してください。 動作確認 ブラウザで確認 ちゃんと反映されてるか確認したい場合、ブラウザの開発者モードのApplicationタブでチェックで来ます。 Android Androidではネイティブアプリと同様に長押しで表示できます。設定したアイコンと名前、リンク先が反映されています。 Windows Windowsの場合はスタートメニューを右クリックもしくはタスクバーのアイコンを右クリックで表示できます。 MacOS Macはやっぱ非対応でした。 ChromeOS ChromeOSではChrome 92以降の実験機能で利用できるそうです。 chrome://flags/#enable-desktop-pwas-app-icon-shortcuts-menu-uiをONにするだけでできます。 公式より↓ かんたんなので皆さんも是非お試しあれ。 【ブログ】https://0115765.com/ 【Twitter】@tomox0115
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android携帯端末でQiitaの記事の誤植を直し始める

我が家では、長らく家族全員iPhoneでした。 Androidは、シミュレータでソフトの試験をしていました。 やっぱ実機がないとねということで、 二人が犠牲になってAndroid携帯端末に変えました。 なかなか使いこなせず、見るだけの生活が続きました。 Twitter, Facebook, Instagramに写真をあげるようになり、 文字入力も慣れてきました。 新人の方によく展開している有益な情報 https://qiita.com/kazuo_reve/items/d1a3f0ee48e24bba38f1 で記事のURLを参照してくださったので、 記事を参照して頂いた時にしていること。 https://qiita.com/kaizen_nagoya/items/0e4e92f45c158ef81423 に、 実際には、1記事の誤字脱字は2−3個程度、常時ある。 誤字脱字を訂正して、ついでに少し追記すると、やっぱり誤字脱字は2−3個程度残る。 と書いたが、誤植訂正だけをする作業をすればいいのではないかと思いついた。 スマフォで自己記事を読んだときに、数文字の訂正は、Android携帯端末ですることにした。 人から紹介してもらった記事に、あまりにも誤字脱字が多いのは、申し訳ないという気持ちから。 編集リクエストは、自分が記事の構造を訂正している最中に送られたときに、わけがわからなくなり、記事がぐちゃぐちゃになってから、なるべく編集リクエストではなく、コメントで誤植訂正をくださいとお願いするようにしている。 gitのような仕組みを使って分岐管理できるのならいいが、 今の Qiitaのシステムでは、複数の編集作業の関係をうまく管理できるような仕組みになっていないのかもしれない。 とにかく、自分で直しだけの作業を始めた。 作業上の課題 文字入力 文字入力で、ボタンが小さく、iとoの打ち間違いが一番多い。 誤植を直したつもりで、誤植を増やしたこともある。 検索 誤植が複数箇所あるときに、同時に直す方法がよくわかっていない。 PCのブラウザでは、検索機能を使って、貼り付けでやっている。 スマフォでまだ効率的にできていない。 URLの貼り付け researchmap, bookmeterなどのSNSがシステムのソフトウェアを更新した際に、参照していた全URLがリンク切れになった。 これを効率的に直すために、もう一つのブラウザのタグからURLの切りはりをするのが効率的にできていない。 作業方法を少し調べて、うまくいったら順次追記します。 <この記事は個人の過去の経験に基づく個人の感想です。現在所属する組織、業務とは関係がありません。>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android+Kotlin null安全が崩れるパターン

v1がnullの場合、Integer.parseInt(null)になる。 var v1: String? = getVal() val v2 = v1.let { // [1] Integer.parseInt(it) // [2]itはnon nullであって欲しい } ?: run{ return // ここには決して処理は来ない } java側の定義が↓こうで、引数のStringにアノテーションが無いからだと思う。 public static int parseInt(String s) throws NumberFormatException { return parseInt(s,10); } kotlinとしては完全に正しい、Integer.parseIntの引数はアノテーションがないからnullの可能性もある だから[2]のitはString?で問題ない。というロジックだろう。 letの前に?を付けてもコンパイルは成立する。 nullableかどうかはkotlinの最重要ポイントなのに、?が有っても無くてもどっちでも意味が通るのが違和感ある。 個人的には、こういう場合は以下のコードに強制して欲しいけどそういうlintあるかな var v1: String? = getVal() val v2 = v1?.let { Integer.parseInt(it) } ?: run{ return // 今回、こっちは実行される可能性ある } というお話でした
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

keyを指定したViewModelの取得もlazy+拡張関数で簡単にしたい

ViewModelを取得するとき、activity-ktxやfragment-ktxで提供されている拡張関数を使うと、ViewModelProviderなどの記述を省略でき、すっきりと記述できます。 HogeActivity.kt private val viewModel: HogeViewModel by viewModels() HogeFragment.kt private val viewModel: HogeViewModel by viewModels() private val viewModel: HogeViewModel by activityViewModels() これは大変便利なのですが、keyを指定することができません。 例えば、ViewPager上のFragmentで使用するViewModelなどはActivityのViewModelStoreを使い、各ページをキーとしたViewModelを使う必要がありますが、この場合には使えないことになります。 まあ、使えなかったところでそこまで複雑な記述になるわけでもないのですが HogeFragment.kt private val viewModel: PageViewModel by lazy { ViewModelProvider(requireActivity()).get("key", PageViewModel::class.java) } ViewModelをkey指定で取得するときはkeyの名前空間に注意しようで書いたように、keyの名前空間への配慮も必要なので、まとめて面倒見てくれる拡張関数が欲しくなります。 既存の仕組みでできないのか ではやり方を調べるため、まずは引数の追加とかでできないか、ソースを追ってみましょう。 ActivityViewModelLazy.kt @MainThread inline fun <reified VM : ViewModel> ComponentActivity.viewModels( noinline factoryProducer: (() -> Factory)? = null ): Lazy<VM> { val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory } return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise) } ViewModelProvider.kt public class ViewModelLazy<VM : ViewModel> ( private val viewModelClass: KClass<VM>, private val storeProducer: () -> ViewModelStore, private val factoryProducer: () -> ViewModelProvider.Factory ) : Lazy<VM> { private var cached: VM? = null override val value: VM get() { val viewModel = cached return if (viewModel == null) { val factory = factoryProducer() val store = storeProducer() ViewModelProvider(store, factory).get(viewModelClass.java).also { cached = it } } else { viewModel } } override fun isInitialized(): Boolean = cached != null } 以上!シンプルですね。ViewModelLazyでViewModelProviderを呼び出していますが、keyを指定する余地がありません。 ないのなら作ってしまおう やっていることは非常にシンプルなので、このViewModelLazyにkeyを指定できるようにしたクラスを用意して、同様にkeyを渡せる拡張関数を作れば良さそうです。 ViewModelExtensions.kt @MainThread inline fun <reified VM : ViewModel> ComponentActivity.keyedViewModels( noinline keyProducer: () -> String, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> = KeyedViewModelLazy( VM::class, keyProducer, { viewModelStore }, factoryProducer ?: { defaultViewModelProviderFactory } ) @MainThread inline fun <reified VM : ViewModel> Fragment.keyedViewModels( noinline keyProducer: () -> String, noinline ownerProducer: () -> ViewModelStoreOwner = { this }, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> = KeyedViewModelLazy( VM::class, keyProducer, { ownerProducer().viewModelStore }, factoryProducer ?: { defaultViewModelProviderFactory } ) @MainThread inline fun <reified VM : ViewModel> Fragment.keyedActivityViewModels( noinline keyProducer: () -> String, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> = KeyedViewModelLazy( VM::class, keyProducer, { requireActivity().viewModelStore }, factoryProducer ?: { requireActivity().defaultViewModelProviderFactory } ) class KeyedViewModelLazy<VM : ViewModel>( private val viewModelClass: KClass<VM>, private val keyProducer: () -> String, private val storeProducer: () -> ViewModelStore, private val factoryProducer: () -> ViewModelProvider.Factory ) : Lazy<VM> { private var cached: VM? = null override val value: VM get() = cached ?: ViewModelProvider(storeProducer(), factoryProducer()) .get( viewModelClass.qualifiedName + ":" + keyProducer(), viewModelClass.java ) .also { cached = it } override fun isInitialized(): Boolean = cached != null } こんなところでしょうか、keyにはモデルクラス名をプレフィックスとしてつけるようにしているので、keyの名前空間についても気にする必要は無くなります。 既存のviewModelsactivityViewModelsと名前がかぶるのはよろしくないのでkeyedをつけています。その関係でkey指定なしには対応していません。なんだかどこかですでにありそうですが。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

keyを指定したViewModelの取得もlazy拡張関数で簡単にしたい

ViewModelを取得するとき、activity-ktxやfragment-ktxで提供されている拡張関数を使うと、ViewModelProviderなどの記述を省略でき、すっきりと記述できます。 HogeActivity.kt private val viewModel: HogeViewModel by viewModels() HogeFragment.kt private val viewModel: HogeViewModel by viewModels() private val viewModel: HogeViewModel by activityViewModels() これは大変便利なのですが、keyを指定することができません。 例えば、ViewPager上のFragmentで使用するViewModelなどはActivityのViewModelStoreを使い、各ページをキーとしたViewModelを使う必要がありますが、この場合には使えないことになります。 まあ、使えなかったところでそこまで複雑な記述になるわけでもないのですが HogeFragment.kt private val viewModel: PageViewModel by lazy { ViewModelProvider(requireActivity()).get("key", PageViewModel::class.java) } ViewModelをkey指定で取得するときはkeyの名前空間に注意しようで書いたように、keyの名前空間への配慮も必要なので、まとめて面倒見てくれる拡張関数が欲しくなります。 既存の仕組みでできないのか ではやり方を調べるため、まずは引数の追加とかでできないか、ソースを追ってみましょう。 ActivityViewModelLazy.kt @MainThread inline fun <reified VM : ViewModel> ComponentActivity.viewModels( noinline factoryProducer: (() -> Factory)? = null ): Lazy<VM> { val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory } return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise) } ViewModelProvider.kt public class ViewModelLazy<VM : ViewModel> ( private val viewModelClass: KClass<VM>, private val storeProducer: () -> ViewModelStore, private val factoryProducer: () -> ViewModelProvider.Factory ) : Lazy<VM> { private var cached: VM? = null override val value: VM get() { val viewModel = cached return if (viewModel == null) { val factory = factoryProducer() val store = storeProducer() ViewModelProvider(store, factory).get(viewModelClass.java).also { cached = it } } else { viewModel } } override fun isInitialized(): Boolean = cached != null } 以上!シンプルですね。ViewModelLazyでViewModelProviderを呼び出していますが、keyを指定する余地がありません。 ないのなら作ってしまおう やっていることは非常にシンプルなので、このViewModelLazyにkeyを指定できるようにしたクラスを用意して、同様にkeyを渡せる拡張関数を作れば良さそうです。 ViewModelExtensions.kt @MainThread inline fun <reified VM : ViewModel> ComponentActivity.keyedViewModels( noinline keyProducer: () -> String, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> = KeyedViewModelLazy( VM::class, keyProducer, { viewModelStore }, factoryProducer ?: { defaultViewModelProviderFactory } ) @MainThread inline fun <reified VM : ViewModel> Fragment.keyedViewModels( noinline keyProducer: () -> String, noinline ownerProducer: () -> ViewModelStoreOwner = { this }, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> = KeyedViewModelLazy( VM::class, keyProducer, { ownerProducer().viewModelStore }, factoryProducer ?: { defaultViewModelProviderFactory } ) @MainThread inline fun <reified VM : ViewModel> Fragment.keyedActivityViewModels( noinline keyProducer: () -> String, noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> = KeyedViewModelLazy( VM::class, keyProducer, { requireActivity().viewModelStore }, factoryProducer ?: { requireActivity().defaultViewModelProviderFactory } ) class KeyedViewModelLazy<VM : ViewModel>( private val viewModelClass: KClass<VM>, private val keyProducer: () -> String, private val storeProducer: () -> ViewModelStore, private val factoryProducer: () -> ViewModelProvider.Factory ) : Lazy<VM> { private var cached: VM? = null override val value: VM get() = cached ?: ViewModelProvider(storeProducer(), factoryProducer()) .get( viewModelClass.qualifiedName + ":" + keyProducer(), viewModelClass.java ) .also { cached = it } override fun isInitialized(): Boolean = cached != null } こんなところでしょうか、keyにはモデルクラス名をプレフィックスとしてつけるようにしているので、keyの名前空間についても気にする必要は無くなります。 既存のviewModelsactivityViewModelsと名前がかぶるのはよろしくないのでkeyedをつけています。その関係でkey指定なしには対応していません。なんだかどこかですでにありそうですが。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ViewModelをkey指定で取得するときはkeyの名前空間に注意しよう

一発ネタです。ViewModelをkey指定で取得するとき、同じkeyで異なるViewModelのインスタンスを取得しようとするとはまります。必ずモデルクラスごとにkeyが異なるように名前空間に注意が必要というお話。 要約 通常ViewModelを取得するときは、以下のようにViewModelProviderから取得します。拡張関数のviewModels()なども最終的には同じことをやっています。 val hogeViewModel = ViewModelProvider(this).get(HogeViewModel::class.java) val fugaViewModel = ViewModelProvider(this).get(FugaViewModel::class.java) 通常、各ViewModelはインスタンスが複数必要なことはないのでこれで良いですが、例えば、ViewPagerの各Fragment用のViewModel等は、FragmentスコープではなくActivityスコープにする必要があり、ページごとに異なるインスタンスをActivityのViewModelStoreに保持してもらう必要があります。そのような場合、getにkeyを指定して、 val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString() val hogeViewModel = ViewModelProvider(this).get(key, HogeViewModel::class.java) のように使います。 さて、このFragmentでViewModelがもう一つ必要になった場合、 val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString() val hogeViewModel = ViewModelProvider(requireActivity()).get(key, HogeViewModel::class.java) val fugaViewModel = ViewModelProvider(requireActivity()).get(key, FugaViewModel::class.java) としてはいけません。 例えば以下のように、keyがモデルクラスごとに異なる値となるような工夫が必要です。 val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString() val hogeViewModel = ViewModelProvider(requireActivity()) .get(HogeViewModel::class.qualifiedName + ":" + key, HogeViewModel::class.java) val fugaViewModel = ViewModelProvider(requireActivity()) .get(HogeViewModel::class.qualifiedName + ":" + key, FugaViewModel::class.java) なぜか ソースコードを追ってみましょう。 ViewModelProvider.java private static final String DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"; @NonNull @MainThread public <T extends ViewModel> T get(@NonNull Class<T> modelClass) { String canonicalName = modelClass.getCanonicalName(); if (canonicalName == null) { throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels"); } return get(DEFAULT_KEY + ":" + canonicalName, modelClass); } @NonNull @MainThread public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) { ViewModel viewModel = mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) { if (mFactory instanceof OnRequeryFactory) { ((OnRequeryFactory) mFactory).onRequery(viewModel); } return (T) viewModel; } else { //noinspection StatementWithEmptyBody if (viewModel != null) { // TODO: log a warning. } } if (mFactory instanceof KeyedFactory) { viewModel = ((KeyedFactory) mFactory).create(key, modelClass); } else { viewModel = mFactory.create(modelClass); } mViewModelStore.put(key, viewModel); return (T) viewModel; } このように、keyを指定しない場合DEFAULT_KEY + ":" + canonicalNameをkeyとしてgetがコールされています。 keyを指定したgetの中身を見てみると、mViewModelStoreからkeyを使って、ViewModelを取り出し、モデルクラスとクラスが一致しているかを確認しています。ViewModelStoreはHashMapのラッパーの用なもので、一つのkeyに一つのインスタンスしか保持できません。 ViewModel viewModel = mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) { ... return (T) viewModel; クラスが異なる場合、その後の処理に流れ、新規でViewModelのインスタンスが作られ、ViewModelStoreにkeyを使って格納されます。当然、そのkeyで格納されていた別のViewModelは上書きされ保持されなくなります。 elseの分岐で// TODO: log a warning.とあるように、エラーログを出す予定らしいですね。 つまり、冒頭で書いたように val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString() val hogeViewModel = ViewModelProvider(requireActivity()).get(key, HogeViewModel::class.java) val fugaViewModel = ViewModelProvider(requireActivity()).get(key, FugaViewModel::class.java) ということをすると、最初にHogeViewModelが作られ、FugaViewModelで上書きされ、次回も同様にと、毎回ViewModelのインスタンスが作り直されることになるため、意図と異なる挙動になってしまいます。 keyを指定しない場合は、内部でクラス名がkeyに使われているので、モデルクラスが異なれば別のkeyで格納され、問題無く動作するだけに、挙動をよく理解しないで使ってしまうとはまってしまいます。 ViewModelは複数使わないということであれば単純なkeyでもかまわないかもしれませんが、少なくとも複数つかう必要がある場合は、keyにモデルクラス名を追加するなどして、名前空間が分かれるように工夫する必要があります。 val pageKey = requireArguments().getInt(ARG_SECTION_NUMBER).toString() val hogeViewModel = ViewModelProvider(requireActivity()) .get(HogeViewModel::class.qualifiedName + ":" + key, HogeViewModel::class.java) val fugaViewModel = ViewModelProvider(requireActivity()) .get(HogeViewModel::class.qualifiedName + ":" + key, FugaViewModel::class.java) さすがにこれは冗長なので、拡張関数などをつくってシンプルにするのが良いでしょう。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidアプリ「RELEASE THE SPYCE secret fragrance」から画像と音声を抽出する

「RELEASE THE SPYCE secret fragrance」は、株式会社オルトプラスが2019年にリリース1したゲームで、すでにサービスが終了しています2。 このゲームにはiPhone版とAndroid版がありますが、本記事はAndroid版アプリを対象とします。この記事は「RELEASE THE SPYCE secret fragrance」の画像と音声データの抽出を目的とするものですが、CRI middlewareを採用する他のスマートフォンゲームでも同様の方法が使えるかもしれません。 著作権に関する注意 この手順を実施して得た画像や音声をインターネット等で配信することは、著作権(公衆送信権)の侵害となる可能性があり、処罰の対象となる可能性があります。私的利用の範囲に留めましょう。 前提条件 このゲーム (jp.co.altplus.spyce) をインストールしたAndroid端末を所持していること その端末のUSBデバッグを有効化していること adbコマンドが使えること gitコマンドが使えること python3がインストールされていること pipがインストールされていること OS: Ubuntu 20.10 (これ以外でも可能だと思いますが、試していません) 作業用ディレクトリを作る mkdir spyce export SPYCE=$(pwd)/spyce 必要なツールをダウンロードする Android Backup Extractor バックアップしたアプリを解凍するツールです。 https://sourceforge.net/projects/adbextractor/ ZIPファイルがダウンロードされます。abe.jarというファイルが含まれていますので、このファイルを適当な場所に保存してください。 UnityPack (画像抽出用) UnityPack形式のファイルから画像データ(PNG形式)を抽出するためのpythonスクリプトです。 クローン cd ${SPYCE} git clone https://github.com/HearthSim/UnityPack.git cd UnityPack git checkout f8cdc2516538d189606a76986ad2d71c3fad5f8b パッチ当て リリフレの画像はETC2というフォーマットになっていますが、UnityPackは対応していません。 そこで、ETC2に対応させるためのパッチを当てます。 次の差分データを etc2.patch として保存してください: diff --git a/bin/unityextract b/bin/unityextract index 3056b0c..3e12ac2 100755 --- a/bin/unityextract +++ b/bin/unityextract @@ -135,6 +135,7 @@ class UnityExtract: image = d.image except NotImplementedError: print("WARNING: Texture format not implemented. Skipping %r." % (filename)) + raise continue if image is None: diff --git a/unitypack/engine/texture.py b/unitypack/engine/texture.py index 35a3b21..652ed17 100644 --- a/unitypack/engine/texture.py +++ b/unitypack/engine/texture.py @@ -94,6 +94,8 @@ IMPLEMENTED_FORMATS = ( TextureFormat.DXT5, TextureFormat.DXT5Crunched, TextureFormat.BC7, + TextureFormat.ETC2_RGB, + TextureFormat.ETC2_RGBA8, ) @@ -158,7 +160,9 @@ class Texture2D(Texture): def image(self): from PIL import Image from decrunch import File as CrunchFile + import etcpack + mode = None if self.format not in IMPLEMENTED_FORMATS: raise NotImplementedError("Unimplemented format %r" % (self.format)) @@ -171,11 +175,19 @@ class Texture2D(Texture): elif self.format == TextureFormat.BC7: codec = "bcn" args = (7, ) + elif self.format == TextureFormat.ETC2_RGB: + codec = "etc2" + args = (1, ) + mode = 'RGB' + elif self.format == TextureFormat.ETC2_RGBA8: + codec = "etc2" + args = (3, ) else: codec = "raw" args = (self.format.pixel_format, ) - mode = "RGB" if self.format.pixel_format in ("RGB", "RGB;16") else "RGBA" + if not mode: + mode = "RGB" if self.format.pixel_format in ("RGB", "RGB;16") else "RGBA" size = (self.width, self.height) data = self.image_data etc2.patch を作成したら、このパッチを適用します: # UnityPackディレクトリ内で実行する patch -p1 < etc2.patch インストール UnityPackをインストールします。 # UnityPack ディレクトリ内で実行する pip3 install . acbpy (音声抽出用) 後述するacb2wavが依存するプログラムです。 cd ${SPYCE} git clone https://github.com/CrescentApricot/acbpy.git cd acbpy pip3 install . hcapy (音声抽出用) 後述するacb2wavが依存するプログラムです。 cd ${SPYCE} git clone https://github.com/CrescentApricot/hcapy.git cd hcapy sed -i "s,.*url =.*,\turl = https://github.com/Cryptomelone/hca2wav.git,g" .gitmodules git submodule sync git submodule update pip3 install . acb2wav (音声抽出用) リリフレの音声はACB(Atom CueSheet Binary)という形式のファイルに格納されています。このファイルからwavファイルとして音声を抽出するために、 acb2wav.py というpythonスクリプトが必要です。 cd ${SPYCE} git clone https://github.com/KOZ39/acb2wav.git ls acb2wav/src/acb2wav.py 画像と音声の抽出手順 アプリをPCにバックアップする リリフレを入れたAndroid端末をPCに接続し、adbコマンドでアプリをPCにバックアップします。 cd ${SPYCE} # spyce.adb というファイル名でアプリをバックアップする adb backup -app -obb -f spyce.adb jp.co.altplus.spyce # Android端末に「フルバックアップ」というウィンドウが # 出ますので「データをバックアップ」をタップしてください。 バックアップファイルをtarファイルに変換する バックアップしたspyce.adbをtarファイルに変換します。 java -jar abe.jar unpack spyce.adb spyce.tar tarファイルを解凍する spyce.tar を解凍します。 apps というディレクトリが作られます。 tar xf spyce.tar test -d apps && echo "apps created" 画像を抽出する 画像は apps/jp.co.altplus.spyce/ef/Android ディレクトリ配下に格納されているファイルに含まれています。 cd ${SPYCE}/apps/jp.co.altplus.spyce/ef/Android ls -l # drwxrwx--x 6 user user 4096 4月 7 00:45 adveffect # drwxrwx--x 170 user user 4096 4月 7 00:48 advunit # drwxrwx--x 2 user user 4096 4月 7 00:45 description # drwxrwx--x 2 user user 4096 4月 7 00:45 eventquestmapeffect # ... UnityPackをインストールしたため、 unityextract コマンドを使用可能な状態になっているはずです。このコマンドを使ってPNGファイルを抽出します。 キャラクターの立ち絵をこのディレクトリの下の texturecharacterstanding のファイルから抽出する例を示します。 cd ${SPYCE}/apps/jp.co.altplus.spyce/ef/Android/texturecharacterstanding unityextract --image * unityextract コマンドを実行すると、同じディレクトリにPNGファイルが作成されます。 11001.png がモモ、 11002.png が雪先輩といった具合になっています。 Android ディレクトリ配下の他のディレクトリのファイルも大体同じようにPNGファイルを抽出できるようです。 音声を抽出する 音声は apps/jp.co.altplus.spyce/ef/OtherAssets/Sounds ディレクトリ配下の各ファイルに格納されており、ACB(Atom CueSheet Binary)という形式になっています。 ACBファイルから生音声(wav)を抽出するために、 acb2wav.py を使用します。 タイトル「リリースザスパイス シークレットフレグランス」を各キャラクターが読み上げている音声を抽出する例を示します。 export ACB2WAV_PY=~/acb2wav/src/acb2wav.py # acb2wav.pyがあるパス # に読み替えてください。 cd ${SPYCE}/apps/jp.co.altplus.spyce/ef/OtherAssets/Sounds cd Title $ACB2WAV_PY -i . -e acb # afs2: 18 files in ar # afs2: aligned to 32 bytes # afs2: a file offset is 4 bytes # Title_002.wav # Title_003.wav # Title_004.wav # Title_005.wav # ... https://www.altplus.co.jp/topics/20190212-m.html ↩ http://www.altplus.co.jp/wp-content/uploads/2020/04/20200401koukoku.pdf ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでスマホゲーム自動化~ファイナルファンタジー ブレイブエクスヴィアス(FFBE)自動化編~

概要 環境の設定が終わり,簡単なアルゴリズムですがFGOの自動化もできました, そこで今回はファイナルファンタジー ブレイブエクスヴィアス(FFBE)のストーリー自動化をしていきたいと思います. 動作確認動画 コードの紹介 スマホから画像を取得してから,テンプレートマッチングしてその結果を元にクリックするだけのコードです. 今回はこれらの画像がキモでした. こんな感じで回ります. .... def story(): _click_image3_vis(r"img\ffbe\story\new.png", offset=(50, 50)) _click_image3_vis(r"img\ffbe\battle\go.png") _click_image3_vis(r"img\ffbe\story\skip2.png") _click_image3_vis(r"img\ffbe\battle\next.png") _click_image3_vis(r"img\ffbe\battle\quest_end.png") # 検証用コード if __name__ == "__main__": while 1: try: story() except: print("!!!!! except !!!!!") time.sleep(1) おわりに 今後はさらに別ゲームへの拡張や戦略アルゴリズム(特にFGO)の開発をやっていく予定です.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む