20210413のAndroidに関する記事は5件です。

SettingsActivity再入門 ~PreferenceFragmentCompatの読み書き対象を変更する~

SettingsActivity再入門 ~PreferenceFragmentCompatで作る設定画面~ の続きです。 PreferenceFragmentCompatはデフォルトの挙動から、DefaultSharedPreferencesに対して読み書きにするためにしか使えないと勘違いされがちですが、デフォルト以外のSharedPreferencesやSharedPreferences以外への読み書きも可能です。 ここでは、PreferenceFragmentCompatの保存先をDefaultSharedPrefrencesから変更する方法を説明します。 読み書き先を変更する PreferenceFragmentCompatはDefaultSharedPreferencesに対して読み書きを行いますが、この挙動は変更することができます。読み書き処理がどのように実装されているのかを確認してみます。 これはPreferenceクラスに全部の型の読み書きが実装されています。Booleanの処理を見てみましょう。 Preference.java protected boolean persistBoolean(boolean value) { if (!shouldPersist()) { return false; } if (value == getPersistedBoolean(!value)) { return true; } PreferenceDataStore dataStore = getPreferenceDataStore(); if (dataStore != null) { dataStore.putBoolean(mKey, value); } else { SharedPreferences.Editor editor = mPreferenceManager.getEditor(); editor.putBoolean(mKey, value); tryCommit(editor); } return true; } protected boolean getPersistedBoolean(boolean defaultReturnValue) { if (!shouldPersist()) { return defaultReturnValue; } PreferenceDataStore dataStore = getPreferenceDataStore(); if (dataStore != null) { return dataStore.getBoolean(mKey, defaultReturnValue); } return mPreferenceManager.getSharedPreferences().getBoolean(mKey, defaultReturnValue); } @Nullable public PreferenceDataStore getPreferenceDataStore() { if (mPreferenceDataStore != null) { return mPreferenceDataStore; } else if (mPreferenceManager != null) { return mPreferenceManager.getPreferenceDataStore(); } return null; } persistBooleanが書き込みで、getPersistedBooleanが読み出し処理ですね。 見ての通り、PreferenceDataStoreがあればそれを、なければPreferenceManagerのSharedPreferencesを利用するようになっていますね。 また、PreferenceDataStoreはPreferenceごとに設定されているものがあればそれを、なければPreferenceManagerのPreferenceDataStoreを利用しています。このことから書き込み先の変更方法は3種類ありそうですね。 ついでに、PreferenceManagerのインスタンスはどのように作られているかですが、PreferenceFragmentCompatのonCreateで作られています。 PreferenceFragmentCompat.java @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); final TypedValue tv = new TypedValue(); getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true); int theme = tv.resourceId; if (theme == 0) { // Fallback to default theme. theme = R.style.PreferenceThemeOverlay; } getActivity().getTheme().applyStyle(theme, false); mPreferenceManager = new PreferenceManager(getContext()); mPreferenceManager.setOnNavigateToScreenListener(this); final Bundle args = getArguments(); final String rootKey; if (args != null) { rootKey = getArguments().getString(ARG_PREFERENCE_ROOT); } else { rootKey = null; } onCreatePreferences(savedInstanceState, rootKey); } onCreatePreferencesはPreferenceManagerが作られた直後にコールされるものなので、PreferenceManagerに対して何か操作をする場合は、onCreatePreferencesの中で行うのが良さそうです。 SharedPreferencesファイルの変更 まずは、標準で使われるSharedPreferencesがどのように作られているのを見てみましょう。PreferenceManager#getSharedPreferences()の実装は以下のようになっています。 PreferenceManager.java public SharedPreferences getSharedPreferences() { if (getPreferenceDataStore() != null) { return null; } if (mSharedPreferences == null) { final Context storageContext; switch (mStorage) { case STORAGE_DEVICE_PROTECTED: storageContext = ContextCompat.createDeviceProtectedStorageContext(mContext); break; default: storageContext = mContext; break; } mSharedPreferences = storageContext.getSharedPreferences(mSharedPreferencesName, mSharedPreferencesMode); } return mSharedPreferences; } PreferenceDataStoreがある場合はnullを返します。SharedPreferencesよりPreferenceDataStoreの方が優先されていることが分かります。PreferenceDataStoreがない場合は、SharedPreferencesのインスタンスを提供しますが、インスタンスが作られていない場合は、ここでmSharedPreferencesNameを使ってSharedPreferencesが作成されます。 mSharedPreferencesNameはコンストラクタで<packagename>_preferencesという、DefaultSharedPreferencesの名前が設定されています。 PreferenceManager.java public PreferenceManager(Context context) { mContext = context; setSharedPreferencesName(getDefaultSharedPreferencesName(context)); } private static String getDefaultSharedPreferencesName(Context context) { return context.getPackageName() + "_preferences"; } 参考までにgetDefaultSharedPreferencesの実装は以下のようになっており、同じ名前になっていることが分かりますね。 PreferenceManager.java public static SharedPreferences getDefaultSharedPreferences(Context context) { return context.getSharedPreferences(getDefaultSharedPreferencesName(context), getDefaultSharedPreferencesMode()); } コンストラクタでもコールしていますが、mSharedPreferencesNameはsetSharedPreferencesNameで変更できます。setSharedPreferencesNameをコールするとmSharedPreferences のインスタンスが廃棄されるので、次回のgetSharedPreferencesで作り直されますね。 PreferenceManager.java public void setSharedPreferencesName(String sharedPreferencesName) { mSharedPreferencesName = sharedPreferencesName; mSharedPreferences = null; } 先に示したように、onCreatePreferencesの先頭であればPreferenceManagerが作られたばかりなので、SharedPreferencesのインスタンスはまだ作られていないので、ここで変更すれば読み書き対象のSharedPrferencesを完全に置き換えることができます。 class SettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.sharedPreferencesName = BuildConfig.APPLICATION_ID + "_main" setPreferencesFromResource(R.xml.root_preferences, rootKey) } } PreferenceManagerのPreferenceDataStoreを指定する SharedPreferencesは使いたくない、他の永続化システムを使いたいんだ、という場合はPreferenceDataStoreを指定します。 先に説明したように、SharedPreferencesよりもPreferenceDataStoreが優先して使われるようになっていますので、読み書きの処理のラッパーを提供すれば任意の永続化システムを使うことができます。 設定する箇所は、onCreatePreferencesの先頭が良いでしょう。 class SettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = MyPreferenceDataStore() setPreferencesFromResource(R.xml.root_preferences, rootKey) } } これで、各Preferenceの読み書きの対象がMyPreferenceDataStoreに置き換わります。 PreferenceDataStoreは以下のように各型ごとに読み書きの処理を実装するabstractクラスになっています。デフォルトの実装は書き込みがUnsupportedOperationExceptionで、読み出しが引数のデフォルト値をそのまま返すというものです。 絶対に使わないことが分かっている型があればその型の実装は省略することができます。 public abstract class PreferenceDataStore { public void putString(String key, @Nullable String value) { throw new UnsupportedOperationException("Not implemented on this data store"); } public void putStringSet(String key, @Nullable Set<String> values) { throw new UnsupportedOperationException("Not implemented on this data store"); } public void putInt(String key, int value) { throw new UnsupportedOperationException("Not implemented on this data store"); } public void putLong(String key, long value) { throw new UnsupportedOperationException("Not implemented on this data store"); } public void putFloat(String key, float value) { throw new UnsupportedOperationException("Not implemented on this data store"); } public void putBoolean(String key, boolean value) { throw new UnsupportedOperationException("Not implemented on this data store"); } @Nullable public String getString(String key, @Nullable String defValue) { return defValue; } @Nullable public Set<String> getStringSet(String key, @Nullable Set<String> defValues) { return defValues; } public int getInt(String key, int defValue) { return defValue; } public long getLong(String key, long defValue) { return defValue; } public float getFloat(String key, float defValue) { return defValue; } public boolean getBoolean(String key, boolean defValue) { return defValue; } } 特定のPreferenceの書き込み先を変更する 前項までの方法ではPreferenceFragmentCompatのPreferenceManagerに対して設定を行うことで全体の挙動を変更していましたが、Preference単位で変更することもできます。 findPreference<SwitchPreferenceCompat>("sync")?.preferenceDataStore = MyPreferenceDataStore() これで特定のPreferenceだけ読み書き先を変更することができます。Preferenceごとに読み書きするデータ型は決まっているため、PreferenceDataStoreの実装もその一つの読み書きだけ実装するでも良さそうです。 以上、PreferenceFragmentCompatの読み書き先の変更方法でした。 先に紹介したようにXMLを記述するだけで簡単に設定画面を作ることができ、その読み書き先も最小限の実装で差し替えることができるので、柔軟な実装ができそうですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SettingsActivity再入門 ~PreferenceFragmentCompatで作る設定画面~

Androidでは設定画面を作る仕組みがJetpackで提供されています。かつてはPreferenceActivityというものがありましたが、いまではPreferenceFragmentCompatがその役割を提供しています。 PreferenceFragmentCompatでは、Androidらしい設定画面を少ない記述で簡単に実現できます。商用プロダクトではあんまり見かけない気がしますが、どうしても標準的な見た目は嫌だとか、これでは実現できない特殊な画面を作らないといけないなどの理由がなければ、商用プロダクトで使っても全然問題のないクオリティの設定画面が作れると思います。 デバッグ用の設定画面とか身内にしか見られないような場所の設定を作る必要があるなら一択でしょう。 また、デフォルトの挙動から、DefaultSharedPreferencesに対して読み書きにするためにしか使えないと勘違いされがちですが、デフォルト以外のSharedPreferencesやSharedPreferences以外への読み書きも可能です。 読み書きの対象を変更する方法についてはこちらを参照ください SettingsActivity再入門 ~PreferenceFragmentCompatの読み書き対象を変更する~ 事始め どういうことができるのかはSettingsActivityというテンプレートが用意されているので作ってみましょう。 Split settings hierarchy into separate sub-screensのチェックボックスをONにすると、カテゴリーとその詳細を別画面にする構成で作られます。 Splitなし Splitあり 子画面 子画面 Splitなしの場合のActivityは以下のように非常にシンプルです。XMLの記述だけでほとんど完結しています。 SettingsActivity.kt class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.settings_activity) if (savedInstanceState == null) { supportFragmentManager .beginTransaction() .replace(R.id.settings, SettingsFragment()) .commit() } supportActionBar?.setDisplayHomeAsUpEnabled(true) } class SettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.root_preferences, rootKey) } } } Splitありの場合はちょっと複雑です。Fragmentの遷移とタイトルの変更のためコードが増えています。 SettingsActivity2.kt private const val TITLE_TAG = "settingsActivityTitle" class SettingsActivity2 : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.settings_activity) if (savedInstanceState == null) { supportFragmentManager .beginTransaction() .replace(R.id.settings, HeaderFragment()) .commit() } else { title = savedInstanceState.getCharSequence(TITLE_TAG) } supportFragmentManager.addOnBackStackChangedListener { if (supportFragmentManager.backStackEntryCount == 0) { setTitle(R.string.title_activity_settings2) } } supportActionBar?.setDisplayHomeAsUpEnabled(true) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putCharSequence(TITLE_TAG, title) } override fun onSupportNavigateUp(): Boolean { if (supportFragmentManager.popBackStackImmediate()) { return true } return super.onSupportNavigateUp() } override fun onPreferenceStartFragment( caller: PreferenceFragmentCompat, pref: Preference ): Boolean { val args = pref.extras val fragment = supportFragmentManager.fragmentFactory.instantiate( classLoader, pref.fragment ).apply { arguments = args setTargetFragment(caller, 0) } supportFragmentManager.beginTransaction() .replace(R.id.settings, fragment) .addToBackStack(null) .commit() title = pref.title return true } class HeaderFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.header_preferences, rootKey) } } class MessagesFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.messages_preferences, rootKey) } } class SyncFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.sync_preferences, rootKey) } } } Fragmentごとにタイトルの変更をしなくて良いのならSplitなしと同等レベルにシンプルになります。(とはいえタイトルが変化しないと分かりにくいですし、PreferenceFragmentCompat.OnPreferenceStartFragmentCallbackの実装は推奨されています。これはあくまで最低限の動作をさせるための確認です) SettingsActivity2.kt class SettingsActivity2 : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.settings_activity) if (savedInstanceState == null) { supportFragmentManager .beginTransaction() .replace(R.id.settings, HeaderFragment()) .commit() } supportActionBar?.setDisplayHomeAsUpEnabled(true) } override fun onSupportNavigateUp(): Boolean { if (supportFragmentManager.popBackStackImmediate()) { return true } return super.onSupportNavigateUp() } class HeaderFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.header_preferences, rootKey) } } class MessagesFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.messages_preferences, rootKey) } } class SyncFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.sync_preferences, rootKey) } } } Prefereceの機能 共通 key PreferenceFragmentCompatはkey/value型のデータを扱う画面であり、各項目はこのkeyに紐付けされます。SharedPreferencesに書き込まれる設定値のkeyとして使用されるほか、値を持たない項目についても、そのPreferenceに対して何らかの操作をコード上からアクセスする場合はこのkeyを使ってPreferencesにアクセするため、設定しておく必要があります。Viewに置けるidのようなものですね。idはint値ですが、keyはStringです。 <Preference app:key="messages" /> title/summary 各項目のタイトルとその説明を設定できます。Titleのみやsummaryのみでも問題ありません。片方だけの場合、高さが少し小さくなります。 <Preference app:title="title" app:summary="summary" /> デフォルト値 Preferenceにはデフォルト値を設定しておくことができます、これは値が書き込まれていない状態に読み出されるデフォルト値ではなく、値が書き込まれていない場合に書き込まれるデフォルト値となります。 PreferenceFragmentCompat#setPreferencesFromResourceをコールしたときに、PreferenceManager経由でPreferenceInflaterが呼び出され、この中でデフォルト値の書き込みが行われます。不適切なデフォルト値を設定しているここでExceptionが発生することになります。 <SwitchPreferenceCompat app:defaultValue="true" /> アイコンスペース テンプレートの画面を見て左側のマージン広すぎでは?と思いませんか? これはアイコンを配置するためのエリアを開けてあるためで、各Preferenceのプロパティにapp:icon=でアイコンを指定できます。また、アイコンはつかないのでマージンは不要という場合はapp:iconSpaceReserved="false"を指定することでマージンを消すことができます。 <Preference app:icon="@drawable/ic_settings" /> <Preference app:iconSpaceReserved="false" /> default app:icon="@drawable/ic_settings" app:iconSpaceReserved="false" Preferenceの依存関係と有効無効 別の設定項目が有効になるまでは、その設定項目は意味をなさないといった依存関係を記述することができます。 app:dependency=で依存先となるKeyを指定します。SwitchPreferenceなどの場合はfalseの場合、EditTextPreferenceの場合値が設定されていない(empty)の場合にdisableになります。ListPreferenceの場合はdisable状態を持ちません。 <Preference app:dependency="sync" /> disabled enabled また、単純に有効無効を設定する場合は、app:enableを使用します <Preference app:enabled="false" /> この有効無効状態を変化させる場合はコード上から設定します。 findPreference<Preference>("hogehoge")?.isEnabled = true Preferenceを非表示にする 設定項目を必要に応じて表示するような用途の場合、app:isPreferenceVisible="false"で非表示にすることができます。 <Preference app:isPreferenceVisible="false" /> 表示させる条件はXMLでは表現できないので、コード上で設定します。 findPreference<Preference>("hogehoge")?.isVisible = true 設定値をsummaryに反映する 現在の設定値がどのような値になっているかに基づいてsummaryの文字列を変更したい場合、それぞれのPreferenceにデフォルトで用意されている機能もありますが、SummaryProviderを実装し、文字列を自分で組み立てることもできます。 SummaryProviderは値が変化したときにコールされ、引数にそのPreferenceが渡されますので、その値に基づいたsummryに表示したいCharSequenceを返却するようにします。 findPreference<EditTextPreference>("hogehoge")?.setSummaryProvider { "設定値は「${(it as EditTextPreference).text}」です" } 当然、summaryを直接書き換えることもできます。 findPreference<EditTextPreference>("hogehoge")?.summary = "hogehoge" 読み書き処理をさせない デフォルトではすべてのPreferenceはkey/valueを書き込む動作となりますが、この処理を停止させることもできます。 <Preference app:persistent="false" /> 設定値と紐付けるのではない、情報表示用などとして利用するPreferencesに読み書きを行わせないようにするときに指定します。 なお、app:persistent="false"に設定した場合でも、Switchの状態などが変化しなくなるわけではありません。 そういうことがしたい場合、次の項目を参考にしてください。 設定値の変化をトリガに処理を実行する 設定値が変化したときに、何らかの処理を実行したい場合、PreferenceにOnPreferenceChangeListenerを設定することで変化のトリガーを受け取ることができます。 findPreference<Preference>("sync")?.setOnPreferenceChangeListener { preference, newValue -> true } また、戻り値としてBooleanを返しますが、このときにfalseを返すと、その値の変更を無効化することができます。 Preferences summary値をコピー可能にする summaryに何らかの情報を表示させておいて、それをコピー可能にさせることができます。 <Preference app:enableCopying="true" /> ロングタップで以下のようなドロップダウンメニューが出てきて、Copyを選択することで、クリップボードにsummaryの内容がコピーされます。 別のfragmentに遷移する SplitでSettingsActivityを作った場合、header_preferences.xmlで使われています。 app:fragmentでfragmentクラス名を指定すると、タップされた際にそのFragmentに遷移するようになります。 <Preference app:fragment="net.mm2d.myapplication.SettingsActivity2$MessagesFragment" /> fragmentを指定しておくだけで、PreferenceFragmentCompatがデフォルトの実装を持っているので、最低限のフラグメントの遷移処理が実行されます。しかし、この処理は利用側が適切に実装することが推奨されています。 実際の実装は以下のようになっており、デフォルトの処理が実行されるときはワーニングログが出力されるようになっていることからも、この実装に任せるのはよろしくなさそうですね。 PreferenceFragmentCompat.java @Override public boolean onPreferenceTreeClick(Preference preference) { if (preference.getFragment() != null) { boolean handled = false; if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) { handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment()) .onPreferenceStartFragment(this, preference); } if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) { handled = ((OnPreferenceStartFragmentCallback) getActivity()) .onPreferenceStartFragment(this, preference); } if (!handled) { Log.w(TAG, "onPreferenceStartFragment is not implemented in the parent activity - " + "attempting to use a fallback implementation. You should " + "implement this method so that you can configure the new " + "fragment that will be displayed, and set a transition between " + "the fragments."); final FragmentManager fragmentManager = requireActivity() .getSupportFragmentManager(); final Bundle args = preference.getExtras(); final Fragment fragment = fragmentManager.getFragmentFactory().instantiate( requireActivity().getClassLoader(), preference.getFragment()); fragment.setArguments(args); fragment.setTargetFragment(this, 0); fragmentManager.beginTransaction() // Attempt to replace this fragment in its root view - developers should // implement onPreferenceStartFragment in their activity so that they can // customize this behaviour and handle any transitions between fragments .replace((((View) getView().getParent()).getId()), fragment) .addToBackStack(null) .commit(); } return true; } return false; } 上記の処理から分かるように、PreferenceFragmentCompatを配置したActivityにOnPreferenceStartFragmentCallbackを実装しておくと、onPreferenceStartFragmentが実行されます。 PreferenceFragmentCompat.java public interface OnPreferenceStartFragmentCallback { boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref); } タブレットなどの広い画面をもつ端末の場合は、ヘッダーとメニューを一つの画面にまとめるといった実装できますね。 Intentを投げる Preferenceタグの中にintentタグを書くことで、そのPreferenceがタップされたときにそのIntentを投げることができます。 以下のように書くと、暗黙的Intentを投げてブラウザを開くことができます。 <Preference> <intent android:action="android.intent.action.VIEW" android:data="https://www.google.com/" /> </Preference> パッケージ名とクラス名を書き、明示的Intentを投げることもできます。またextraタグでextraの設定も可能です。 <Preference> <intent android:targetClass="net.mm2d.myapplication.SettingsActivity2" android:targetPackage="net.mm2d.myapplication" > <extra android:name="HOGE" android:value="FUGA" /> </intent> </Preference> ActivityNotFoundExceptionが発生した場合、catchすることができないので、受け手が必ず存在するIntent以外はこの方法を使わない方が良いでしょう。 クリックイベントを実装する 前項までのようにクリック時の動作をXMLで指定することもできますが、より複雑な処理を実装したい場合は、コード上からOnPreferenceClickListenerを設定することで実装することができます。 戻り値はbooleanでtrueを返却するとクリックイベントが消費され、他のクリック時の処理が発火しなくなります。 findPreference<Preference>("sync")?.setOnPreferenceClickListener { true } 処理の優先度としては、OnPreferenceClickListener > fragment > intent という順序になっているようです。 SwitchPreferenceCompat Boolean値を扱うPreferenceです。同様にBooleanを扱うPreferenceはTwoStatePreferenceのサブクラスとして実装されており、他にSwitchPreferenceとCheckboxPreferenceをほぼ同じように使うことができます。違いは名前の通り、ON/OFFの表現が、SwitchCompatかCheckboxかSwitchかの違いですね。ON/OFFの表現が複数あるのは分かりにくくなりますし、SwitchPreferenceCompatを使うのが良いかと思います。 ON/OFFでsummaryを変更する TwoStatePreferenceは取り得る値が2つしかないので、その2パターンの文字列をXMLで設定しておくことで、ON/OFFに応じてsummaryを変更してくれます <SwitchPreferenceCompat app:summaryOff="@string/attachment_summary_off" app:summaryOn="@string/attachment_summary_on" /> EditTextPreference 任意のString値を扱うPreferenceです。タップするとダイアログが開いてユーザーが任意のテキストを入力できます。 設定値をsummryに表示する 設定値はStringなので、一番シンプルなsummaryの表示は、設定値をそのまま表示するものです。 この動作で問題無い場合は、SummaryProviderを実装する必要はありません。 app:useSimpleSummaryProvider="true"を指定することでこの動作となります。 <EditTextPreference app:useSimpleSummaryProvider="true" /> ダイアログのテキストを変更する EditTextPreferenceに限りませんが、ダイアログが開くタイプのPreferenceの場合はタイトル、メッセージ、ボタンのテキストを変更できます。 <EditTextPreference app:dialogTitle="dialogTitle" app:dialogMessage="dialogMessage" app:positiveButtonText="positiveButtonText" app:negativeButtonText="negativeButtonText" /> ListPreference String値を扱うPreferenceです。タップするとダイアログが開き複数の項目から一つの項目を選択することができます。 設定値と選択肢、デフォルト値 デフォルト値はstringで指定可能です。 ダイアログに表示する選択肢はapp:entriesで、それが選択された際に実際に書き込まれる値をapp:entryValuesで指定します。 <ListPreference app:defaultValue="reply" app:entries="@array/reply_entries" app:entryValues="@array/reply_values" /> いずれもstring-arrayのリソースとして定義して使用します。当然のことながら要素数を同じにしておく必要があります。 entriesの方が少ない場合は、選択できない値が出てくるだけですが、逆だとArrayIndexOutOfBoundsExceptionが発生してしまいます。 arrays.xml <string-array name="reply_entries"> <item>Reply</item> <item>Reply to all</item> </string-array> <string-array name="reply_values"> <item>reply</item> <item>reply_all</item> </string-array> 設定値をsummryに表示する EditTextPreferenceと同様、設定値はStringなので、一番シンプルなsummaryの表示は、設定値をそのまま表示するものです。 この動作で問題無い場合は、SummaryProviderを実装する必要はありません。 app:useSimpleSummaryProvider="true"を指定することでこの動作となります。 <EditTextPreference app:useSimpleSummaryProvider="true" /> MultiSelectListPreference Setを扱うPreferenceです。タップするとダイアログが開き、複数の項目から任意個の項目を選択することができます。 設定値と選択肢、デフォルト値 設定値と選択肢はListPreferenceと同じですが、デフォルト値をstringで指定することはできません、string-arrayで指定可能です。 設定値をsummryに表示する MultiSelectListPreferenceはapp:useSimpleSummaryProvider="true"を使うことができません。必要であればSummaryProviderを実装する必要があります。(文字列連結で表示する機能があっても良さそうですが) findPreference<MultiSelectListPreference>("reply")?.summaryProvider = Preference.SummaryProvider<MultiSelectListPreference> { it.values.joinToString() } SeekBarPreference int値を扱うPreferenceです。SeekBarの操作でint値を設定します。 最大値、最小値 最大値と最小値を指定することができますが、ネープスペースプレフィックスが異なります、最小値はapp:minで、最大値はandroid:maxで指定します。 <SeekBarPreference app:min="0" android:max="50" /> 現在値の表示 デフォルトでは現在値の表示は行われませんが、app:showSeekBarValue="true"を指定することでシークバー横に現在値が表示されるようになります。 <SeekBarPreference app:showSeekBarValue="true" /> PreferenceCategory 複数のPreferenceをグループ化するための仕組みです。titleがカテゴリー名として表示され、Categoryの間にはdividerが表示されます。タイトルだけでなく、説明などを追加したい場合はsummaryを指定することもできます。 PreferenceCategoryは入れ子にすることもできますが、あまり意味は無いと思います。 <PreferenceCategory app:title="@string/messages_header"> </PreferenceCategory> 折りたたんだ状態で表示する PreferenceCategoryにapp:initialExpandedChildrenCountを指定することで、ここで指定した個数以上のPreferenceを折りたたんでおくことができます。タップすることで展開されますが、再度折りたたむことはできません。 <PreferenceCategory app:initialExpandedChildrenCount="0"> 展開前 展開後 PreferenceFragmentComaptでできることをざっと紹介しました。すべてを紹介しきったわけではなく標準的な仕組みだけですが、これだけでも結構いろんなことが簡単にできそうですよね。 設定画面を自前で作ったことのある人なら分かると思いますが、各UIパーツを配置して、設定を反映、タップなどの操作を受け取り~と想像以上にたくさんの実装が必要です。それがXMLを書くだけでほとんどの機能を実装できてしまいますし、Android的でシステムUI等と親和性の高いマテリアルデザインのUIが特に意識することなく実現できますので、実装コストが低く、メンテナンス性も良いと思います。 積極的に使っていきましょう。 引き続き、以下の記事もどうぞ。 SettingsActivity再入門 ~PreferenceFragmentCompatの読み書き対象を変更する~
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kotlin inline 関数内で発生した例外のスタックトレース

概要 クラッシュレポートを調べていて、スタックトレースに書かれた行番号がそのソースファイルに存在しないことがありました。 検索してみると、以下のページを見つけました。 inline 関数で展開されるコードで例外が発生した場合、そのスタックトレースにはソースファイルの行数を超えた行番号が書かれるようです。 試してみる 例1. inline 関数で例外を投げてみる 以下のようなコードを作成しました。例外を投げる inline 関数 throwErrorFunc を実行し、例外をキャッチしスタックトレースをログに出力します。 MainActivity.kt 1: package com.example.inlineexceptionapp 2: 3: import androidx.appcompat.app.AppCompatActivity 4: import android.os.Bundle 5: 6: class MainActivity : AppCompatActivity() { 7: override fun onCreate(savedInstanceState: Bundle?) { 8: super.onCreate(savedInstanceState) 9: setContentView(R.layout.activity_main) 10: 11: try { 12: throwErrorFunc() 13: } 14: catch (e: Exception) { 15: println(e.stackTraceToString()) 16: } 17: } 18: 19: 20: inline fun throwErrorFunc() { 21: throw Exception("error") 22: } 23: } 実行した際に出力されたログ I/System.out: java.lang.Exception: error I/System.out: at com.example.inlineexceptionapp.MainActivity.onCreate(MainActivity.kt:24) I/System.out: at android.app.Activity.performCreate(Activity.java:7009) I/System.out: at android.app.Activity.performCreate(Activity.java:7000) I/System.out: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214) I/System.out: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2731) I/System.out: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856) I/System.out: at android.app.ActivityThread.-wrap11(Unknown Source:0) I/System.out: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589) I/System.out: at android.os.Handler.dispatchMessage(Handler.java:106) I/System.out: at android.os.Looper.loop(Looper.java:164) I/System.out: at android.app.ActivityThread.main(ActivityThread.java:6494) I/System.out: at java.lang.reflect.Method.invoke(Native Method) I/System.out: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) I/System.out: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) MainActivity.kt には 23 行しかありませんが、スタックトレースには at com.example.inlineexceptionapp.MainActivity.onCreate(MainActivity.kt:24) とあります。MainActivity.kt に存在しない行番号が出力されていることがわかります。 例2. forEach 関数 for など列挙中のコレクションに対して add や remove を行い、コレクションの要素数を変更した場合、ConcurrentModificationException が発生します。 以下のコードを作成しました。 MainActivity.kt 1: package com.example.inlineexceptionapp 2: 3: import androidx.appcompat.app.AppCompatActivity 4: import android.os.Bundle 5: 6: class MainActivity : AppCompatActivity() { 7: override fun onCreate(savedInstanceState: Bundle?) { 8: super.onCreate(savedInstanceState) 9: setContentView(R.layout.activity_main) 10: 11: try { 12: val list = mutableListOf(1, 2, 3) 13: list.forEach { 14: // 何かしらの処理 15: 16: // 誤って列挙中のコレクションを変更してしまう 17: list.add(1) 18: 19: // 何かしらの処理 20: } 21: } 22: catch (e: Exception) { 23: println(e.stackTraceToString()) 24: } 25: } 26: } 実行した際に出力されたログ I/System.out: java.util.ConcurrentModificationException I/System.out: at java.util.ArrayList$Itr.next(ArrayList.java:860) I/System.out: at com.example.inlineexceptionapp.MainActivity.onCreate(MainActivity.kt:27) I/System.out: at android.app.Activity.performCreate(Activity.java:7009) I/System.out: at android.app.Activity.performCreate(Activity.java:7000) I/System.out: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214) I/System.out: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2731) I/System.out: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856) I/System.out: at android.app.ActivityThread.-wrap11(Unknown Source:0) I/System.out: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589) I/System.out: at android.os.Handler.dispatchMessage(Handler.java:106) I/System.out: at android.os.Looper.loop(Looper.java:164) I/System.out: at android.app.ActivityThread.main(ActivityThread.java:6494) I/System.out: at java.lang.reflect.Method.invoke(Native Method) I/System.out: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) I/System.out: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) at com.example.inlineexceptionapp.MainActivity.onCreate(MainActivity.kt:27) とあり、やはり MainActivity.kt に存在しない 27 行で発生したことになっています。 原因 概要に挙げたリンクに以下のようにあります。 This means that the actual source line is an inlined code fragment from somewhere else. As class files (in the proper debug information) only support specifying a single source file kotlin had to use a workaround. Basically it adds a table in the class file with mappings between line number ranges and source files. The line numbers used for this are outside the actual range of line numbers of the file. The debugger/ide will “fix” it up for you, but exceptions don’t do that. (Google 翻訳) これは、実際のソース行が別の場所からのインライン化されたコードフラグメントであることを意味します。クラスファイル(適切なデバッグ情報内)は単一のソースファイルの指定のみをサポートしているため、kotlinは回避策を使用する必要がありました。基本的に、行番号範囲とソースファイル間のマッピングを含むテーブルをクラスファイルに追加します。これに使用される行番号は、ファイルの実際の行番号の範囲外です。デバッガー/ IDEはそれを「修正」しますが、例外はそれを行いません。 デバッグ目的のため、inline 展開されるコードには実際のソースファイルの範囲外の行番号を付けているようです。 クラスファイルに行番号が追加されているようです。 確認してみる 例2 で使用したコードで確認してみます。 1: Kotlin Bytecode を表示してみる Bytecode を表示してみました。以下の手順でソースコードの Bytecode を表示することができます。 * Tools -> Kotlin -> Show Kotlin Bytecode ソースコード 13 行にカーソルを合わせると、その行に対応する Bytecode がハイライトされます。 Bytecode 87行以降は java.util.List.dd を実行していることから、Bytecode 67-87 行が展開された forEach のコードのようです。 Bytecode 67, 72 行に LINENUMBER 27 とあり、スタックトレースにある行番号と一致します。 2: class ファイルを逆アセンブルしてみる LineNumberTable はそのバイトコードとソース上の行番号の対応がかかれています。LocalVariableTable は、メソッド内で宣言した変数の名前などを保持したものです。 LineNumberTable と LocalVariableTable は主にデバッグ時に用いられ、実行時には不必要な属性です。 class ファイルを逆アセンブルし、LineNumberTable を確認すると良さそうです。 class ファイルは \app\build\tmp\kotlin-classes\debug\com\example\inlineexceptionapp\MainActivity.class にあります。以下のコマンドを実行して、逆アセンブルします。 $ javap -verbose -l -c MainActivity.class 出力結果です。 // 省略 protected void onCreate(android.os.Bundle); descriptor: (Landroid/os/Bundle;)V flags: ACC_PROTECTED Code: stack=4, locals=9, args_size=2 0: aload_0 1: aload_1 2: invokespecial #11 // Method androidx/appcompat/app/AppCompatActivity.onCreate:(Landroid/os/Bundle;)V 5: aload_0 6: ldc #12 // int 2131427356 8: invokevirtual #16 // Method setContentView:(I)V 11: iconst_5 12: anewarray #18 // class java/lang/Integer 15: dup 16: iconst_0 17: iconst_1 18: invokestatic #22 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 21: aastore 22: dup 23: iconst_1 24: iconst_2 25: invokestatic #22 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 28: aastore 29: dup 30: iconst_2 31: iconst_3 32: invokestatic #22 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 35: aastore 36: dup 37: iconst_3 38: iconst_4 39: invokestatic #22 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 42: aastore 43: dup 44: iconst_4 45: iconst_5 46: invokestatic #22 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 49: aastore 50: invokestatic #28 // Method kotlin/collections/CollectionsKt.mutableListOf:([Ljava/lang/Object;)Ljava/util/List; 53: astore_2 54: nop 55: aload_2 56: checkcast #30 // class java/lang/Iterable 59: astore_3 60: iconst_0 61: istore 4 63: aload_3 64: invokeinterface #34, 1 // InterfaceMethod java/lang/Iterable.iterator:()Ljava/util/Iterator; 69: astore 5 71: aload 5 73: invokeinterface #40, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 78: ifeq 118 81: aload 5 83: invokeinterface #44, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 88: astore 6 90: aload 6 92: checkcast #46 // class java/lang/Number 95: invokevirtual #50 // Method java/lang/Number.intValue:()I 98: istore 7 100: iconst_0 101: istore 8 103: aload_2 104: iconst_1 105: invokestatic #22 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 108: invokeinterface #56, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z //省略 LineNumberTable: line 8: 0 line 9: 5 line 11: 11 line 12: 54 line 13: 55 line 27: 63 line 27: 71 line 17: 103 line 20: 114 line 28: 118 line 22: 122 line 23: 123 line 24: 143 line 25: 143 LineNumberTable より、Bytecode とソースコードを以下のように対応させているようです。 Bytecode 55 行 -> ソースコード 13 行 Bytecode 63-103 行 -> ソースコード 27 - 行 Bytecode 103 行 -> ソースコード 17 行 Bytecode 63-103 行 は Kotlin Bytecode で見たように Iterator , Iterator.hasNext が見られます。forEach で展開されるコードを ソースコードの 27 行としているのは間違いなさそうです。 まとめ クラッシュレポートなど、スタックトレースでソースファイルの範囲外の行番号が出力されていた場合は、inline 関数を実行している個所を疑ってみましょう。 ソースファイルの Kotlin Bytecode を表示させてスタックトレースに書かれた行番号を検索することで、該当箇所を見つけられるかもしれません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

@JvmOverloadsとは

JavaからKotlinのデフォルト値を持つクラス、または関数を呼び出せるようにするアノテーション。 Javaでデフォルト値はサポートされていないため、以下のGreeting#sayHelloを呼び出すとコンパイルエラーになります。 NG class Greeting { fun sayHello(prefix: String = "Mr.", name: String) { println("Hello, $prefix $name") } } public class JavaClass { public static void main(String[] args) { Greeting greeting = new Greeting(); // prefixを引数に渡していないのでエラーになる。 greeting.sayHello("Bob"); } } Greeting#sayHelloに@JvmOverloadsを付与することで、Javaからデフォルト値を持つ関数を呼び出せるようになります。 OK class Greeting { @JvmOverloads fun sayHello(prefix: String = "Mr.", name: String) { println("Hello, $prefix $name") } } public class JavaClass { public static void main(String[] args) { Greeting greeting = new Greeting(); greeting.sayHello("Bob"); } } 参考 developers/デフォルトの関数オーバーロード Calling Kotlin Code from Java
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cordovaの画面を固定サイズで実装する方法

固定の画面サイズで実装されているWebアプリをCordova 10に移植する場合の実装方法です。 ※CordovaはAndroidのみです。iOSは使用していません。ですが環境に限らず同じ方法は使えるはずです。 例えば300x400の固定サイズで実装したWebアプリがあるとします。 それをCordova上でスマホの端末の画面いっぱいに拡大表示することで、固定レイアウトのままAndroidアプリを作ってしまう方法です。Webアプリ移植時の変更を最小限にすることができます。 固定幅で作りたいだけならviewportを使う方法もありますが、幅も高さも画面からはみ出ないように柔軟に拡大するためJavaScriptで実装しました。 拡大表示する方法 CSSのtransformを使います。 以下のような記述をすると要素全体を拡大表示できることを利用します。 /* 150%に拡大する場合の例 */ transform: translate(25%, 25%) scale(1.5, 1.5); これをJavaScriptで動的に比率を計算して行います。 以下はメインformを画面サイズに合わせて拡大する部分のコードです。(記事の最後に全ソースがあります。) ※Webアプリのサイズが300x400(余白を入れると320x400)の場合のコードです。 ※formでなくてもdiv等でも同じやり方で拡大できます。 // 画面サイズに合わせて拡大 var windowWidth = (window.innerWidth || document.documentElement.clientWidth || 0); var windowHeight = (window.innerHeight || document.documentElement.clientHeight || 0); let percent = Math.floor(100 * windowWidth / 320); if (windowWidth > windowHeight) { percent = Math.min(percent, Math.floor(100 * windowHeight / 400)); } let scale = percent / 100; let translate = "" + ((percent - 100) / 2) + "%"; if (scale > 1) { mScale = scale; // グローバル変数に拡大率を保存しておく ※後で説明 document.forms[0].style.transform = "translate(" + translate + ", " + translate + ") " + "scale(" + scale + ", " + scale + ")"; } 固定サイズ座標取得方法 タップした位置の座標は固定サイズの仮想的な座標とはことなるので、マウスの座標のようにタップ位置を取得したい場合は座標変換が必要になります。 以下の部分が拡大表示したときの拡大率を使って座標を変換する部分のコードです。(記事の最後に全ソースがあります。) // イベントでの固定サイズ座標取得サンプル let getTouchPos = function(toucheInfo) { let orgX = toucheInfo.pageX; let orgY = toucheInfo.pageY; // ※mScale: 拡大表示したときの拡大率 // ※mAdjustX,mAdjustY: 基準位置(左上)の実際の座標 let x = (toucheInfo.pageX - mAdjustX) / mScale; let y = (toucheInfo.pageY - mAdjustY) / mScale; return { orgX, orgY, x, y }; }; document.addEventListener("touchmove", function(){ event.preventDefault(); // デフォルトのイベントを禁止する let pos = getTouchPos(event.touches[0]); // TODO 何かの処理 // ※pos.x, pos.yで固定サイズの座標が取得できます。 }, passiveSupported ? { passive: false } : false); 実際のCordovaのサンプルコード 画面を固定サイズ化(拡大)するサンプルコードを紹介します。 Webアプリのメイン領域を300x300の固定サイズで実装する場合のコードです。 Cordova(10.0)のindex.htmlに上書きすればサンプルの確認ができます。 index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'"/> <meta name="format-detection" content="telephone=no"> <meta name="msapplication-tap-highlight" content="no"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> <meta name="color-scheme" content="light dark"> <link rel="stylesheet" href="css/index.css"> <title>固定レイアウト画面サンプル</title> <script> window.onerror = function(msg, uri, line) { alert(msg + uri + line); }; var mAdjustX = 0; var mAdjustY = 0; var mScale = 1; // Passive eventサポート有無のチェック var passiveSupported = false; try { window.addEventListener("test", null, Object.defineProperty({}, "passive", { get: function() { passiveSupported = true; } })); } catch(err) {} // ------------------------------------------------------------------------------------------------ // 初期化処理 // ------------------------------------------------------------------------------------------------ function initialize() { // フォーム表示によりずれる座標を調整する if (document.mainForm.offsetLeft) { mAdjustX = document.mainForm.offsetLeft; mAdjustY = document.mainForm.offsetTop; } else { mAdjustX = 0; mAdjustY = 0; } // イベントでの固定サイズ座標取得サンプル let getTouchPos = function(toucheInfo) { let orgX = toucheInfo.pageX; let orgY = toucheInfo.pageY; let x = (toucheInfo.pageX - mAdjustX) / mScale; let y = (toucheInfo.pageY - mAdjustY) / mScale; return { orgX, orgY, x, y }; }; let getMousePos = function(event2, event) { let orgX, orgY, x, y; if (event2.toString() == "[object MouseEvent]") { orgX = event2.clientX; orgY = event2.clientY; x = (event2.clientX - mAdjustX) / mScale; y = (event2.clientY - mAdjustY) / mScale; } else if(event) { orgX = event.clientX; orgY = event.clientY; x = (event.clientX - mAdjustX) / mScale; y = (event.clientY - mAdjustY) / mScale; } return { orgX, orgY, x, y }; }; document.addEventListener("touchstart", function(){ let pos = getTouchPos(event.touches[0]); // TODO 何かの処理 }, passiveSupported ? { passive: false } : false); document.addEventListener("touchend", function(){ let pos = getTouchPos(event.changedTouches[0]); // TODO 何かの処理 }, passiveSupported ? { passive: false } : false); document.addEventListener("touchmove", function(){ event.preventDefault(); // デフォルトのイベントを禁止する let pos = getTouchPos(event.touches[0]); // TODO 何かの処理 el("info").innerText = "本当の座標:(" + pos.orgX + "," + pos.orgY + ") 固定サイズ座標:(" + Math.floor(pos.x) + "," + Math.floor(pos.y) + ")"; }, passiveSupported ? { passive: false } : false); document.addEventListener("mousedown", function(event2){ let pos = getMousePos(event2, event); // TODO 何かの処理 }, false); document.addEventListener("mouseup", function(event2){ let pos = getMousePos(event2, event); // TODO 何かの処理 }, false); document.addEventListener("mousemove", function(event2){ let pos = getMousePos(event2, event); // TODO 何かの処理 el("info").innerText = "本当の座標:(" + pos.orgX + "," + pos.orgY + ") 固定サイズ座標:(" + Math.floor(pos.x) + "," + Math.floor(pos.y) + ")"; }, false); // 画面サイズに合わせて調整 var windowWidth = (window.innerWidth || document.documentElement.clientWidth || 0); var windowHeight = (window.innerHeight || document.documentElement.clientHeight || 0); let percent = Math.floor(100 * windowWidth / 320); if (windowWidth > windowHeight) { percent = Math.min(percent, Math.floor(100 * windowHeight / 400)); } let scale = percent / 100; let translate = "" + ((percent - 100) / 2) + "%"; if (scale > 1) { mScale = scale; document.forms[0].style.transform = "translate(" + translate + ", " + translate + ") " + "scale(" + scale + ", " + scale + ")"; } let gameScreen = el("gameScreen") let wallWidth = (windowWidth - gameScreen.getBoundingClientRect().width) / 2; el("mainDiv").style.paddingLeft = "" + wallWidth + "px"; mAdjustX = wallWidth; mAdjustY += el("scoreArea").getBoundingClientRect().height; } // ------------------------------------------------------------------------------------------------ // HTMLエレメント取得 // ------------------------------------------------------------------------------------------------ function el(strId) { return document.getElementById(strId); } </script> <style> body { font-family: sans-serif; overflow-y: hidden; } * { user-select: none; -ms-user-select:none; -moz-user-select:none; -webkit-user-select: none; -webkit-touch-callout: none; } #mainDiv { background-color: #bbbbff; text-transform: none; } .label { display: inline-block; text-transform: none; } </style> </head> <body onload="initialize();"> <!-- メイン --> <div id="mainDiv" style="width:100%; height:100%;"> <form name="mainForm" style="position:relative; width:100%; height:100%;" onsubmit="return false;"> <!-- 情報表示 --> <div> <div id="scoreArea" style="height:36px; color:#000080; font-weight:bold;"> <div style="font-size:100%; margin-top:2px; line-height:17px;"> <div class="label">TODO 情報表示エリア1</div><br> <div class="label">スコアの表示等</div> </div> </div> </div> <!-- スクリーン表示 --> <div> <div id="gameScreen" style="width:300px; height:300px; position:relative; background-color:#cccccc;"> TODO ここは300x300の固定サイズで実装可能 <div style="position:absolute; width:100px; height:100px; top: 50px; left: 50px; background:green; ">top: 50px;<br>left: 50px;</div> <div style="position:absolute; width:100px; height:100px; top:100px; left:100px; background:yellow;">top:100px;<br>left:100px;</div> <div style="position:absolute; width:100px; height:100px; top:150px; left:150px; background:red; ">top:150px;<br>left:150px;</div> </div> </div> <!-- 情報表示2 --> <div> <div id="scoreArea2" style="height:36px; color:#000080; font-weight:bold;"> <div style="font-size:80%; margin-top:2px; opacity:0.5;"> <div class="label" style="font-size:90%;">TODO 情報表示エリア2</div><br> <div class="label" id="info">スコア以外の表示等</div> </div> </div> </div> </form> </div> <script src="cordova.js"></script> <script src="js/index.js"></script> </body> </html> 実際にアプリを移植した例 実際に画面固定サイズで実装していたWebアプリ(ブロック崩しゲーム)をCordovaに移植してAndroidアプリを作成しました。 「HRGシンプルブロック崩し」としてGoogle Playで公開しています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む