20210724のAndroidに関する記事は4件です。

Androidのシンプルアーキテクチャ

この記事では、Androidアプリのアーキテクチャについて、改めて纏めています。 ポイント Androidに関わらず、オブジェクト思考ではよく言われる、下記を守ることです。 クラスをシンプルに 依存を少なく 技術要素 ここでは、下記の技術要素を使用しています。 Androidの推奨アーキテクチャ https://developer.android.com/jetpack/guide?hl=ja#recommended-app-arch データバインディング https://developer.android.com/topic/libraries/data-binding?hl=ja AndroidでのDI https://developer.android.com/training/dependency-injection?hl=ja DataStore https://developer.android.com/topic/libraries/architecture/datastore?hl=ja 実装テーマ メモ画面。 保存先は、DataStore。 クラス構成 構成は、下記を参考に、MemoFragment、MemoViewModel、MemoRepositoryです。 依存方向:MemoFragment → MemoViewModel → MemoRepository 実装内容  MemoFragment ポイントはDataBinding。 DataBindingで、UI(xml)とソース(ここでは、ViewModel)を繋ぎます。 MemoFragment class MemoFragment : Fragment() { private lateinit var binding: DataStoreFragmentBinding private val viewModel: MemoViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = DataBindingUtil.inflate(inflater, R.layout.data_store_fragment, container, false) binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = viewModel return binding.root } ... DataBindingを使うために、app/build.gradleに下記を追加します。 app/build.gradle dataBinding { enabled = true } layoutファイルは、layout階層を追加します。 dataには、MemoViewModelを追加します。 EditTextにandroid:text="@={viewModel.text}"を記載して、MemoViewModelのLiveDataと双方向にバインドさせます。(双方向のポイントは@と{の間の=) Buttonには、android:onClick="@{() -> viewModel.onClick()}"を記載して、クリック時のメソッドをバインドします。 data_store_fragment.xml <layout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="com.ykato.sample.kotlin.datastore.MemoViewModel" /> </data> ... <EditText android:layout_width="match_parent" android:layout_height="0dp" android:gravity="top" android:text="@={viewModel.text}" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/guideline" /> ... <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/save_button" android:onClick="@{() -> viewModel.onClick()}"/> ...  MemoViewModel ViewModelはDI(Dependency Injection)を使用することで、依存度を下げます。 MemoRepositoryをメンバ変数として保持する場合、コンストラクタの引数としてInjectionすることで依存が下がります。(Mockもしやすく、テストもしやすくなります。) MemoViewModel.kt @HiltViewModel class MemoViewModel @Inject constructor( private val store: MemoRepository ): ViewModel() { ... DI(Hilt, Dagger)を行うために、依存関係をgradleファイルに追加します。 build.gradle buildscript { ... dependencies { ... classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' ... } } app/build.gradle ... apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' ... android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } ... } ... dependencies { implementation "com.google.dagger:hilt-android:2.35" kapt "com.google.dagger:hilt-android-compiler:2.35" } ... また、DIを行う為には、Applicationクラスに@HiltAndroidAppを追加する必要があります。 KotlinSampleApplication.kt @HiltAndroidApp class KotlinSampleApplication: Application() MemoFragmentとMainActivityには@AndroidEntryPointを追加する必要があります。 (MemoFragmentをMainActivity上に追加しているため、MainActivityにも追加する必要がある。) MainActivity.kt @AndroidEntryPoint class MainActivity : AppCompatActivity() { ... MemoFragment.kt @AndroidEntryPoint class MemoFragment : Fragment() { ...  MemoRepository MemoRepositoryはDataStoreを使用して、メモの内容を保存します。 また、メモの内容をキャッシュする(DataStoreの読み込み・保存は非同期処理なので、キャッシュして同期メソッドを実装する)ため、Singletonにしています。 MemoRepository.kt val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "memo") val MEMO_STRING = stringPreferencesKey("memo_string") @Singleton class MemoRepository @Inject constructor( @ApplicationContext private val context: Context, ) { private val cache by lazy { AtomicReference<String?>(null) } ... DataStoreを使用するために、依存に下記を追加 app/build.gradle ... dependencies { implementation("androidx.datastore:datastore-preferences:1.0.0-rc01") ... 参考 今回のコードは下記にあります。 https://github.com/yoshihiro-kato/android-samples/tree/05a032ade61babbc8cf133418fc367a5a091a33f/KotlinSampleApplication/app/src/main/java/com/ykato/sample/kotlin/datastore
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActivityResultContract を使ってアプリ外からアクセスできる写真を撮影する (対象範囲別ストレージ対応)

ActivityResultContract を使って、外部のカメラアプリを起動して写真を撮影します。 Android 10 以上の対象範囲別ストレージ (Scoped Storage) 環境で、他のアプリからアクセスできるようにメディア領域へ写真を保存します。 Android 9 以下でも権限や MediaStoreAPI の処理が少しだけ異なる以外はほとんど同じ実装となります。 カメラアプリを起動して撮影した写真は、なるべく外部のアプリからもアクセスできることが求められます。ライフログ系のコンテンツやブログ記事の添付ファイルなどコンテンツ性のある写真であれば、ファイルをアップロードするだけでなく、ユーザーの端末のメディア領域へ保存し、アプリがアンインストールされても写真が消えないようにすることが求められます。 撮影した写真をアプリ内だけで利用し、他のアプリからアクセスできないように扱いたい場合は、以下の記事を参照してください。 実現方法 TakePictureContract を作成し、カメラアプリからアプリ固有ストレージへ写真を保存したあと、メディア領域へ保存することで他のアプリからアクセスできるようにします。 以下の 3 つの記事の組み合わせで対応します。本記事では実装について詳細な説明は省いています。詳細な解説はそれぞれの記事に書かれていますので、参照してください。 前提 AndroidX を導入していること。 OS バージョン 外部ストレージの権限の扱いをシンプルにするため、Android 4.4 以上のみサポートすることを想定します。 権限について Android 10 以上では、外部のカメラアプリからアプリ固有ストレージへ写真を書き込んだあと、メディア領域へ書き込むため、必要な権限はありません。 カメラアプリを起動するため、CAMERA 権限も不要ですが、AndroidManifest.xml に CAMERA 権限を定義している場合のみ、CAMERA 権限のランタイムパーミッション取得が必要です。 Android 9 以下ではメディア領域への書き込みに WRITE_EXTERNAL_STORAGE 権限が必要です。TakePictureContract を実行する前に WRITE_EXTERNAL_STORAGE 権限を取得するように実装しています。 実装 カメラアプリがアプリ固有ストレージへ書き込めるように、FileProvider の設定を追加します。 src/main/res/xml/provider_path.xml <paths> <!-- external-files-path: getExternalFilesDir() 用の宣言 --> <external-files-path name="external-files" path="." /> <!-- file-path: getFilesDir() 用の宣言 --> <file-path name="files" path="." /> </paths> AndroidManifest.xml に FileProvider の設定と、WRITE_EXTERNAL_STORAGE 権限の宣言を記載します。 AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="..."> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <application android:theme="@stylve/AppTheme"> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_path" /> </provider> <!-- ... --> </application> </manifest> storeMediaImage() を実装します。以下の記事のものと全く同じものです。 suspend fun Context.storeMediaImage( imageUri: Uri, fileName: String, mimeType: String // "image/jpeg" など ): Uri = withContext(Dispatchers.IO) { val collection = if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { // データ書き込みの場合は MediaStore.VOLUME_EXTERNAL_PRIMARY が適切 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI } // collection = "content://media/external/images/media" のような Content URI // destination = "content://media/external/images/media/{id}" のような Content URI val destination = contentResolver.insert(collection, ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, fileName) put(MediaStore.Images.Media.MIME_TYPE, mimeType) if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { put(MediaStore.Images.Media.IS_PENDING, true) } } ) ?: throw IllegalStateException("保存メディアファイルの作成に失敗") var input: InputStream? = null var output: OutputStream? = null try { input = contentResolver.openInputStream(imageUri) ?: error("画像ファイルを開けない") output = contentResolver.openOutputStream(destination) ?: error("保存メディアファイルを開けない") input.copyTo(output) } catch (e: FileNotFoundException) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { contentResolver.delete(destination, null, null) } throw IllegalStateException(e) } catch (e: IOException) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { contentResolver.delete(destination, null, null) } throw e } finally { input?.close() output?.close() } if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { contentResolver.update(destination, ContentValues().apply { put(MediaStore.Images.Media.IS_PENDING, false) }, null, null) } destination } メディア領域への保存に対応した TakePictureContract を実装します。 class TakePictureContract( owner: SavedStateRegistryOwner, savedStateKey: String, private val contextProvider: () -> Context ) : ActivityResultContract<String, suspend () -> String?>() { private var cameraOutputFile: File? = null init { owner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = owner.savedStateRegistry registry.registerSavedStateProvider(savedStateKey) { bundleOf("output" to cameraOutputFile?.toString()) } registry.consumeRestoredStateForKey(savedStateKey)?.getString("output")?.let { output -> cameraOutputFile = File(output) } } }) } override fun createIntent(context: Context, input: String): Intent { val file = File( // /storage/emulated/0/Android/data/.../files/Pictures/{input} のようなアプリ固有の外部ストレージ context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) // /data/user/0/.../files のようなアプリ固有の内部ストレージ ?: context.filesDir , input ).also { cameraOutputFile = it } file.createNewFile() // uri: content://{packageName}.fileprovider/external-files/Pictures/{input} のような Content URI val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, uri) } } override fun parseResult(resultCode: Int, intent: Intent?): suspend () -> String? { return suspend { var result: String? = null if (resultCode == Activity.RESULT_OK) { val file = requireNotNull(cameraOutputFile) result = try { contextProvider().storeMediaImage( Uri.fromFile(file), file.name, "image/jpeg" ).toString() } catch (e: IllegalStateException) { // ファイルが存在しないなど null } catch (e: IOException) { // ファイル読み書き失敗 null } } cameraOutputFile?.delete() cameraOutputFile = null result } } } TakePictureContract を使用してカメラアプリでの写真撮影を実行します。 class MyFragment: Fragment() { private val takePictureLauncher = registerForActivityResult( // savedStateKey 他の savedStateKey と被らなければ OK TakePictureContract(this, "takePicture") { requireContext() } ) { result -> lifecycleScope.launch { val file = result() if (file != null) { // file = content://media/external/images/media/{id} のような Content URI } else { // 撮影がキャンセルされた } } } private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { takePicture() } else { // 権限を取得できなかった } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = /* ... */ binding.button.setOnClickListener { if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { takePicture() } else { // Android 9 以下ではメディア領域への保存に WRITE_EXTERNAL_STORAGE 権限が必要 requestPermissionLauncher.launch(WRITE_EXTERNAL_STORAGE) } } } private fun takePicture() { // ファイル名は getExternalFilesDir() や filesDir() の既存のファイルと被らないものを渡す takePictureLauncher.launch("image.jpg") } // ... 解説 詳しい解説はそれぞれの記事を参照してください。 ActivityResultContract.parseResult() は Coroutine に対応していないため、結果を suspend () -> String? として評価を遅延させています。結果を受け取る callback 側で適切な CoroutineScope を使って結果を受け取るようにしています。 TakePictureContract.parseResult() はディスク容量が足りないなどでメディア領域へのファイル保存に失敗する可能性があるため、エラー処理が必要になるかもしれません。 class TakePictureContract( owner: SavedStateRegistryOwner, savedStateKey: String, private val contextProvider: () -> Context, private val errorHandler: ((e: Exception) -> Unit)? = null ) : ActivityResultContract<String, suspend () -> String?>() { // ... override fun parseResult(resultCode: Int, intent: Intent?): suspend () -> String? { // ... } catch (e: IllegalStateException) { // ファイルが存在しないなど errorHandler(e) null } catch (e: IOException) { // ファイル読み書き失敗 errorHandler(e) null } // ... } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android: ActivityResultContract を使ってアプリ外からアクセスできる写真を撮影する (対象範囲別ストレージ対応)

ActivityResultContract を使って、外部のカメラアプリを起動して写真を撮影します。 Android 10 以上の対象範囲別ストレージ (Scoped Storage) 環境で、他のアプリからアクセスできるようにメディア領域へ写真を保存します。 Android 9 以下でも権限や MediaStoreAPI の処理が少しだけ異なる以外はほとんど同じ実装となります。 カメラアプリを起動して撮影した写真は、なるべく外部のアプリからもアクセスできることが求められます。ライフログ系のコンテンツやブログ記事の添付ファイルなどコンテンツ性のある写真であれば、ファイルをアップロードするだけでなく、ユーザーの端末のメディア領域へ保存し、アプリがアンインストールされても写真が消えないようにすることが求められます。 撮影した写真をアプリ内だけで利用し、他のアプリからアクセスできないように扱いたい場合は、以下の記事を参照してください。 実現方法 TakePictureContract を作成し、カメラアプリからアプリ固有ストレージへ写真を保存したあと、メディア領域へ保存することで他のアプリからアクセスできるようにします。 以下の 3 つの記事の組み合わせで対応します。本記事では実装について詳細な説明は省いています。詳細な解説はそれぞれの記事に書かれていますので、参照してください。 前提 AndroidX を導入していること。 OS バージョン 外部ストレージの権限の扱いをシンプルにするため、Android 4.4 以上のみサポートすることを想定します。 権限について Android 10 以上では、外部のカメラアプリからアプリ固有ストレージへ写真を書き込んだあと、メディア領域へ書き込むため、必要な権限はありません。 カメラアプリを起動するため、CAMERA 権限も不要ですが、AndroidManifest.xml に CAMERA 権限を定義している場合のみ、CAMERA 権限のランタイムパーミッション取得が必要です。 Android 9 以下ではメディア領域への書き込みに WRITE_EXTERNAL_STORAGE 権限が必要です。TakePictureContract を実行する前に WRITE_EXTERNAL_STORAGE 権限を取得するように実装しています。 実装 カメラアプリがアプリ固有ストレージへ書き込めるように、FileProvider の設定を追加します。 src/main/res/xml/provider_path.xml <paths> <!-- external-files-path: getExternalFilesDir() 用の宣言 --> <external-files-path name="external-files" path="." /> <!-- files-path: filesDir 用の宣言 --> <files-path name="files" path="." /> </paths> AndroidManifest.xml に FileProvider の設定と、WRITE_EXTERNAL_STORAGE 権限の宣言を記載します。 AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="..."> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <application android:theme="@stylve/AppTheme"> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_path" /> </provider> <!-- ... --> </application> </manifest> storeMediaImage() を実装します。以下の記事のものと全く同じものです。 suspend fun Context.storeMediaImage( imageUri: Uri, fileName: String, mimeType: String // "image/jpeg" など ): Uri = withContext(Dispatchers.IO) { val collection = if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { // データ書き込みの場合は MediaStore.VOLUME_EXTERNAL_PRIMARY が適切 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI } // collection = "content://media/external/images/media" のような Content URI // destination = "content://media/external/images/media/{id}" のような Content URI val destination = contentResolver.insert(collection, ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, fileName) put(MediaStore.Images.Media.MIME_TYPE, mimeType) if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { put(MediaStore.Images.Media.IS_PENDING, true) } } ) ?: throw IllegalStateException("保存メディアファイルの作成に失敗") var input: InputStream? = null var output: OutputStream? = null try { input = contentResolver.openInputStream(imageUri) ?: error("画像ファイルを開けない") output = contentResolver.openOutputStream(destination) ?: error("保存メディアファイルを開けない") input.copyTo(output) } catch (e: FileNotFoundException) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { contentResolver.delete(destination, null, null) } throw IllegalStateException(e) } catch (e: IOException) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { contentResolver.delete(destination, null, null) } throw e } finally { input?.close() output?.close() } if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { contentResolver.update(destination, ContentValues().apply { put(MediaStore.Images.Media.IS_PENDING, false) }, null, null) } destination } メディア領域への保存に対応した TakePictureContract を実装します。 class TakePictureContract( owner: SavedStateRegistryOwner, savedStateKey: String, private val contextProvider: () -> Context ) : ActivityResultContract<String, suspend () -> String?>() { private var cameraOutputFile: File? = null init { owner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = owner.savedStateRegistry registry.registerSavedStateProvider(savedStateKey) { bundleOf("output" to cameraOutputFile?.toString()) } registry.consumeRestoredStateForKey(savedStateKey)?.getString("output")?.let { output -> cameraOutputFile = File(output) } } }) } override fun createIntent(context: Context, input: String): Intent { val file = File( // /storage/emulated/0/Android/data/.../files/Pictures/{input} のようなアプリ固有の外部ストレージ context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) // /data/user/0/.../files のようなアプリ固有の内部ストレージ ?: context.filesDir , input ).also { cameraOutputFile = it } file.createNewFile() // uri: content://{packageName}.fileprovider/external-files/Pictures/{input} のような Content URI val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, uri) } } override fun parseResult(resultCode: Int, intent: Intent?): suspend () -> String? { return suspend { var result: String? = null if (resultCode == Activity.RESULT_OK) { val file = requireNotNull(cameraOutputFile) result = try { contextProvider().storeMediaImage( Uri.fromFile(file), file.name, "image/jpeg" ).toString() } catch (e: IllegalStateException) { // ファイルが存在しないなど null } catch (e: IOException) { // ファイル読み書き失敗 null } } cameraOutputFile?.delete() cameraOutputFile = null result } } } TakePictureContract を使用してカメラアプリでの写真撮影を実行します。 class MyFragment: Fragment() { private val takePictureLauncher = registerForActivityResult( // savedStateKey 他の savedStateKey と被らなければ OK TakePictureContract(this, "takePicture") { requireContext() } ) { result -> lifecycleScope.launch { val file = result() if (file != null) { // file = content://media/external/images/media/{id} のような Content URI } else { // 撮影がキャンセルされた } } } private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { takePicture() } else { // 権限を取得できなかった } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = /* ... */ binding.button.setOnClickListener { if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { takePicture() } else { // Android 9 以下ではメディア領域への保存に WRITE_EXTERNAL_STORAGE 権限が必要 requestPermissionLauncher.launch(WRITE_EXTERNAL_STORAGE) } } } private fun takePicture() { // ファイル名は getExternalFilesDir() や filesDir() の既存のファイルと被らないものを渡す takePictureLauncher.launch("image.jpg") } // ... 解説 詳しい解説はそれぞれの記事を参照してください。 ActivityResultContract.parseResult() は Coroutine に対応していないため、結果を suspend () -> String? として評価を遅延させています。結果を受け取る callback 側で適切な CoroutineScope を使って結果を受け取るようにしています。 TakePictureContract.parseResult() はディスク容量が足りないなどでメディア領域へのファイル保存に失敗する可能性があるため、エラー処理が必要になるかもしれません。エラー処理はたとえば以下のように実装できます。 class TakePictureContract( owner: SavedStateRegistryOwner, savedStateKey: String, private val contextProvider: () -> Context, private val errorHandler: ((e: Exception) -> Unit)? = null ) : ActivityResultContract<String, suspend () -> String?>() { // ... override fun parseResult(resultCode: Int, intent: Intent?): suspend () -> String? { // ... } catch (e: IllegalStateException) { // ファイルが存在しないなど errorHandler(e) null } catch (e: IOException) { // ファイル読み書き失敗 errorHandler(e) null } // ... } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】 TextWatcherを使って、金額入力時にカンマを加える。

今回の目的 EditTextで金額を入力する時、月並みだが、動的に入力した数値をカンマ区切りに変換できないか、 色々調べたところ、ある程度制御が出来るようになったので、紹介していこうと思います。 使用する環境 AndroidStudio Ver 3.4.1 Android 10.0 (Q) API 29 TextWatcher TextWatcher (AndroidDeveloper) ⇨ EditTextの入力を監視するEventListener(編集前の値を取得したり、編集後の値やポジションを取得する等、出来る) 色々詳しいことは、参考サイトや↑のリファレンスを参照してください。 実装に当たって まずはImplements(実装)していきます。 MainActivity.java public class SampleClass1 extends AppCompatActivity implements TextWatcher その後、エラーが出て、「専用のクラスをOverRideせよ」と言われるのでどこかに以下のクラス群を設置します。 MainActivity.java @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { } とここまでは、調べると色々出てくるので、詳しい内容は参考サイトを見てみてください。 実装 では実際に実装していきます MainActivity.java @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { //入力直後のテキストを取得 if (!chg_flg){ prevText = charSequence.toString(); } } @Override public void onTextChanged(CharSequence charSequence, int cur, int del, int add) { try { if (chg_flg) { return; } //変更途中はスルー StringBuilder str_claft = new StringBuilder(); String str = charSequence.toString(); //カンマを消そうとしているかチェックする if (!this.CheckInputCash(prevText, str)) { //カンマ手前の数字を削除する cur -= 1; str_claft.append(str); str_claft.deleteCharAt(cur); str = str_claft.toString(); } //ここでflgを立てておく //この対策をしておかないと、後々2重で呼び出されることになってめんどくさい。 chg_flg = true; //ここでカンマを一旦外す。 str = str.replace(",", ""); int kngk = 0; String tmp_kngk = ""; //現在のカーソル位置を編集用変数に格納。 int sel_pos = cur; //文字数が3文字以上の場合、カンマ区切りに整形する。 if (str.length() > 3) { kngk = Integer.parseInt(str); tmp_kngk = String.format("%,d", kngk); } else { //0からスタートの場合は、「0」を削除する。 if (prevText.equals("0")) { str = str.replace("0", ""); } tmp_kngk = str; } if (cur == 0 && del != 0 && tmp_kngk.length() == 0) { //削除時、0桁になる場合は「0」をセットする。 tmp_kngk = "0"; sel_pos = 1; } else { //追加時、追加後の文字数によってPostionを移動する。 if (del == 0){ if (tmp_kngk.length() == 9 || tmp_kngk.length() == 5) { // 特定の文字数の時は+2      ex)1,234,567 or 1,234 sel_pos += 2; } else { // それ以外は+1 sel_pos += 1; } } } //範囲以上になる場合は、末尾にPostionをセット。 if (sel_pos > tmp_kngk.length()) { sel_pos = tmp_kngk.length(); } //新規と編集でEditTextの参照先が変わる場合は、MODE管理をしておく。 //変わらないであれば、if文自体が要らない。 if (form_mode.equals("00")){ INPUT_KNGK.setText(tmp_kngk); INPUT_KNGK.setSelection(sel_pos); }else if(form_mode.equals("01")){ EDIT_KNGK.setText(tmp_kngk); EDIT_KNGK.setSelection(sel_pos); } //終わったら、flgを戻す chg_flg = false; } catch (Exception e) { e.printStackTrace(); } } @Override public void afterTextChanged(Editable editable) { try{ if (!chg_flg){return;} //変更途中はスルー String str = editable.toString(); //ここでチェック機能を走らせることも出来る。 if (str.length() > 11){ MSGFORM1.setVisibility(View.VISIBLE); ERRMSG1.setVisibility(View.VISIBLE); ERRMSG1.setText("入力できる桁数を超えています。9桁以内で入力してください。"); }else{ MSGFORM1.setVisibility(View.GONE); ERRMSG1.setVisibility(View.GONE); } } catch (Exception e) { e.printStackTrace(); } } MainActivity.java /** 削除対象がカンマかどうかチェックする **/ public boolean CheckInputCash(String before,String after){ boolean result = true; try{ String str_before = before.replace(",",""); String str_after = after.replace(",",""); if (str_before.length() == str_after.length()){ //カンマ外して文字数が一緒ならば、カンマを削除しようとしている result = false; }else{ result = true; } } catch (Exception e) { e.printStackTrace(); } return result; } ポイント カンマ区切りに整形する場合、カーソルの持って行き場を指定しないといけないので、そこが大変。 カーソルがカンマに当たっている状態で削除キーを押すと、後の方にカーソルが当たってしまうので、制御してあげないといけない。 というより、何が一番大変ってカーソルの位置指定のめんどくささ。 チェック機能をつけるであれば、「afterTextChanged()」で行う。 それ以外だと、変な感じの挙動になる。 EditTextはPrivateで扱えるように定義してあげないといけない。 整形後の内容を入れるため。 終わりに どうでしょうか? カーソルの扱い方は慣れないと難しいですが、デバッグして引数の値を見ていくと、色々掴めてきます。 他のサイトで見るよりかは簡単に実装できたように感じますが、他に何か参考になりそうなサイトがあれば、ぜひコメントください。 それでは、また。 参考サイト 入力を監視するTextWatcher/nyanのアプリ開発 個人的にはここのサイトがオススメです。(わかりやすい) TextWatcherでリアルタイムに値を取得する方法(Android)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む