Androidはワンツーパンチ 三歩進んで二歩下がる

プログラミングやどうでもいい話

Androidアプリ「投資名言」リリースしました!

こんにちは。 2019年11月、個人開発のAndroidアプリ「投資名言」をリリースしました。
投資に関する名言ですが、人生に応用できるようなよい言葉がたくさん入っているアプリです。
ネットワークアクセスもほとんど無い電池に優しいアプリですので是非お試しください(^^)

play.google.com

それにしても私らしく地味なアプリです(笑)
画面も黒っぽいです。
機能はシンプルです。毎日通知が届くのでそこから名言の解説画面に遷移するような使い方になると想定しています。

f:id:sakura_bird1:20191202214925p:plain:w500

アプリを作る前はこんな感じの画面遷移図を作って大体のイメージを作るようにしています。
この図はSketchというMacのアプリで作成しました。

f:id:sakura_bird1:20191202215804p:plain:w500

こういうのを作る度にもっとデザイン方面に力を入れていきたい、とずっと思っています。


Android開発の技術面では、全体的に新しいコンポーネントを使っています。

  • Android Jetpack
  • Kotlin
  • LiveData
  • Navigation
  • Room
  • Kotlin Coroutines
  • Dagger2

などなど試行錯誤で作りました。
最近のコンポーネントの習得はほとんどGoogle Developers Codelabsで学習しました。
しかしながら進化が早いため、教材と最新版がずれてしまうことがザラであり進めるのはコツがいる感じでした。

今後はiOS版も作りたいと思います。
読んでくれてありがとうございました。

【AndroidX】Jetpack のNavigation で戻るボタンの処理をカスタマイズする(Backキーのイベントを拾う, OnBackPressedDispatcher, OnBackPressedCallback, ToolBarの戻るボタン)

AndroidJetpackのNavigationコンポーネントを使って画面遷移をハンドリングしているアプリで、戻るボタンが押された場合の処理をカスタマイズする必要が出た時に、最近出た新しい方法で戻るボタンの処理を書きました。
その時のメモです。
Navigationコンポーネント内の機能というわけではないので、Navigationを使っていないアプリでもonBackPressedメソッドをoverrideする代わりにその方法を使えます。
おまけでActionBarのToolBarでも戻る矢印のボタンを使っている場合のハンドリングについても書いています。

公式サイト

実行した環境

  • Android Studio 3.5
  • com.android.tools.build:gradle:3.5.0
  • Kotlin 1.3.50
  • androidx.appcompat:appcompat:1.1.0-rc01
  • androidx.navigation:navigation-fragment-ktx:2.2.0-alpha02
  • androidx.navigation:navigation-ui-ktx:2.2.0-alpha02

参考サイト様

何がやりたいのか

JetpackNavigation componentを使って戻るボタンの処理をNavigationに任せている場合でも、カスタマイズしたい時があります。

例えば何らかの処理を行わないでユーザーが画面を離脱しようとした時に、警告ダイアログを表示するなどが考えられます。

古くからある戻るボタンのハンドリング方法は、Activityクラスの中でonBackPressedメソッドをoverrideして、その中に処理を記述していましたが、個々のFragmentから見ると親のActivityに処理を任せなくてはならず使い勝手がよくありませんでした。

Jetpackで登場したComponentActivityOnBackPressedDispatcherを使うと、戻るボタンのイベントを受け取って処理を記述できるようになります。
Activityにコードを書かずに済むし、有効と無効の切り替えも簡単に出来ます。

onBackPressedメソッドのオーバーライドの代わりにこれを使って処理を書いてみます。

注意事項

  • AndroidX 移行済みの前提です。
  • Jetpackは進化が早いので、この記事も古くなっているかも知れません。参考にされる場合はご了承ください。
  • この記事では実装はKotlinのみを使用しています。Javaでの実装は扱っておりません。

必要なライブラリ

AndroidXのActivity が必要となります。
2019年4月25日にリリースされたandroidx.activity:activity:1.0.0-alpha07で大幅にアップデートされたバックボタンのハンドリングの機能を使います。

その後も変更やメソッド削除等があるので、androidx.activity:activity:1.0.0androidx.activity:activity-ktx:1.0.0の安定版以降を使うのがよいと思います。

公式サイトのAndroidXのActivityの導入ガイドはこちらです。
Activity  |  Android Developers

依存ライブラリのバージョンの確認

少し話がずれますが、androidx.activity:activityのライブラリは、
androidx.appcompat:appcompatandroidx.navigation:navigationからも依存されています。

そのためわざわざbuild.gradleのdependenciesに記述しなくてもandroidx.activity:activityのパッケージを使用出来ます。
ですが、少し古いリリースのものだとまだ実装されていないかもしれないので、一応バージョンの確認をしておくといいでしょう。

依存関係の確認方法の一例として、下記のコマンドをプロジェクトのルート配下で実行すると、depends.txtというテキストファイルが出来ます。
これを開いてバージョンを確認します。

$ ./gradlew app:dependencies > depends.txt

次の画像は手元で実行した時のスクリーンショットです。

f:id:sakura_bird1:20190919123514p:plain:w500

androidx.activityで検索すると、この例ではandroidx.activity:activity:1.1.0-alpha03 (*)となっています。
1.0.0以降ですのでバージョンはOKです。

色々なライブラリがandroidx.activityを参照していてバージョンが異なったとしても、Gradleの依存関係のルールでは参照されてる中で最も最新のものが全てに強制的に適用されます。(カスタマイズも可能)
Gradleの依存関係のルールについては公式サイトをご覧ください。

戻るボタンの処理を記述する

OnBackPressedDispatcherにコールバックを追加する

公式サイトより引用します。

class MyFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // This callback will only be called when MyFragment is at least Started.
        val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
            // Handle the back button event
        }

        // The callback can be enabled or disabled here or in the lambda
    }
    ...
}

説明(ほぼ公式サイトより)

FragmentActivityAppCompatActivityの継承元クラスであるComponentActivityでは、getOnBackPressedDispatcher()を呼び出して取得できるOnBackPressedDispatcherを使用して戻るボタンのハンドリングを行うことが出来ます。

OnBackPressedDispatcheraddCallback()メソッドを呼び出されるとOnBackPressedCallback オブジェクトを返してhandleOnBackPressed()メソッド内で戻るボタンのイベントをキャッチした時の処理を書くことが出来ます。

コールバックの引数にはLifecycleOwnerを渡します。
OnBackPressedCallbackLifecycleOwnerの状態がLifecycle.State.STARTEDになってから追加されます。

複数のコールバックを登録できます。
登録された順の逆の順序で有効なコールバックが呼び出されます。

コールバックの有効と無効を切り替える

isEnabled()メソッドで切り替えます。デフォルトでtrueになっています。

callback.isEnabled = false

コールバックはChain of Responsibility パターンに従っています。

コールバックの有効と無効を切り替えることは、呼び出し順序を維持するためにも一時的な変更にすることがおすすめです。
特に複数のネストされたLifecycleOwnerにコールバックが追加されている場合は特に重要です。

OnBackPressedCallbackを削除する

OnBackPressedCallbackを完全に削除する場合は、remove()メソッドを呼び出す必要があります。
ただし、コールバックは関連付けられているLifecycleOwnerが 破棄されると自動的に削除されるため、通常は必要ありません。

ActivityのonBackPressedメソッド

ActivityonBackPressedメソッドをoverrideする方法をとっている場合は、
OnBackPressedCallbackを代わりに使うのがおすすめです。
この変更を行うことができない場合は、次の規則が適用されます。

  • addCallbackで登録されたコールバックは、super.onBackPressed()を呼び出した時に評価される
  • onBackPressed()はOnBackPressedCallbackのインスタンスに関係なく常に呼び出される。

Navigationで一つ前の画面に戻る

Navigationのバックスタックを一つ遡るには、NavController.popBackStack()メソッドを使います。
特定の画面に遷移したい場合は公式サイトをご覧ください。

        requireActivity().onBackPressedDispatcher.addCallback(this@MyFragment) {
            // Handle the back button event
            ...

            findNavController().popBackStack()
        }

ActionBarのUpボタンの処理(おまけ)

NavigationUI.setupActionBarWithNavController()を使用してNavigationコンポーネントとアクションバーの挙動を結びつけると、NavigationがToolBarの領域に戻る矢印のUpボタンなど状況に応じて適切なナビゲーションを表示してくれるようになります。 (レイアウト内のfragmentタグで「app:defaultNavHost="true"」となっている前提です)

f:id:sakura_bird1:20190920223516p:plain:w300

参考コード

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // レイアウト内のfragmentタグで「app:defaultNavHost="true"」となっている前提

        setContentView(main_activity)

        val host: NavHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ?: return
        val navController = host.navController

        setSupportActionBar(binding.toolbar)

        val appBarConfiguration = AppBarConfiguration(navController.graph)

        setupActionBarWithNavController(navController, appBarConfiguration)
    }

    override fun onSupportNavigateUp(): Boolean =
        findNavController(R.id.nav_host_fragment).navigateUp()
}

Upボタンのイベントを取得して処理をカスタマイズする場合は
Fragment内でonOptionsItemSelectedメソッドをオーバーライドしてMenuItemのidが android.R.id.homeの時にUpボタンが押された時の処理を記述します。

class MyFragment : Fragment() {

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                // Upボタンが押された時の処理

                // falseを指定すると、Upボタンの処理が続行されて前の画面に戻る
                // trueを指定した時は自分でfindNavController().popBackStack()などを実行して前の画面に戻る
                false
            }
            R.id.action_share -> {
                // 何らかの処理
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}

Sketchで背景をテキストでマスクする&背景の中のテキスト部分を透過させる方法

Sketchを使って画像やシェイプを文字の形に切り抜くやり方と、
画像やシェイプの中のテキストの部分を透明にするやり方のメモです。
次の画像のようなものを作ります。

f:id:sakura_bird1:20190804120943p:plain
出来上がり

公式サイト

参考サイト様

①シェイプの中のテキスト部分を透明にする

Rectangleのシェイプのレイヤーの上にテキストのレイヤーを重ねます。
テキストはそのままでもいいし、Convert to Outlinesでパスにしてもいいです。

f:id:sakura_bird1:20190804121530p:plain:w500

テキストレイヤーとシェイプのレイヤーを同時に選択します。

f:id:sakura_bird1:20190804121942p:plain

メニューからLayerCombineSubtract 又は Differenceを選択します。

f:id:sakura_bird1:20190804123115p:plain:w500

するとCombined Shape というレイヤーになりテキスト部分が透過されます。

f:id:sakura_bird1:20190804141750p:plain:w500

②画像の中のテキスト部分を透明にする

画像で行う場合は、画像レイヤーの上にテキストのレイヤーを重ねてCombined Shapeを作ってもうまくいきません。
シェイプの背景を画像にした後にCombined Shapeを作ります。

まずRectangleのシェイプを作ります。(説明のためRectangleのシェイプにしていますが、Ovalなどの形でも大丈夫です)
右側のスタイル設定の中の Fills から Pattern Fill をクリックし、
IMAGES をクリックして画像を読み込み、DisplayFill にします。

f:id:sakura_bird1:20190804124044p:plain:w400

上の説明画像とは違う背景ですが、シェイプの背景が設定されました。

f:id:sakura_bird1:20190804124421p:plain:w500

後は ①シェイプの中のテキスト部分を透明にすると同じように、
テキストレイヤーを作成し、 テキストレイヤーとシェイプのレイヤーを同時に選択した状態で
メニューからLayerCombineSubtract 又は Differenceを選択すれば Combined Shape というレイヤーになりテキスト部分が透過されます。

f:id:sakura_bird1:20190804125312p:plain:w500

③背景画像をテキストでマスクして画像を文字の形に切り抜く

参考サイト様がとても詳しく説明してくださっているので、そちらを読むのが一番よいと思いますが、
私の手元で試したものを軽く説明させていただきます。

Advanced Text Mask In Sketch - Design + Sketch - Medium

③-1 Combined Shapeによるマスク

まず、②画像の中のテキスト部分を透明にする と同様に
シェイプを作成し、背景画像を設定します。
テキストレイヤーも作成します。

テキストレイヤーとシェイプのレイヤーを同時に選択した状態で
メニューからLayerCombineIntersectを選択すれば Combined Shape というレイヤーになり背景がテキストの形にくり抜かれたようになります。

f:id:sakura_bird1:20190804131227p:plain:w500

f:id:sakura_bird1:20190804131402p:plain:w500

この方法だと、テキストレイヤーを背景の上の方や下の方に配置しても背景の中央にテキストがあるかのように
背景の中央部分が切り抜かれるようです。
切り抜きの位置を柔軟に変えたければ、次に説明するやり方のほうがよいと思います。

③-2 Combined Shapeレイヤーの上に画像を重ねてマスクする

今までのやり方と違い、テキストをマスク用のシェイプにして、その上に画像を重ねてマスクします。
例えば画像を丸くくり抜くような普通のマスク方法と似たような感じです。
テキスト部分と画像が結合していないので、マスク位置を柔軟に変更することが出来ます。

まずはテキストの形をした透明のシェイプレイヤーを作ります。
Rectangleのシェイプとテキストのレイヤーを作ります。
テキストの色はなんでもいいので、目立つ色にしてます。

f:id:sakura_bird1:20190804134953p:plain:w500

Rectangleのシェイプを非表示にします。

f:id:sakura_bird1:20190804135142p:plain:w500

テキストレイヤーとシェイプのレイヤーを同時に選択した状態で
メニューからLayerCombineUnionを選択します。

f:id:sakura_bird1:20190804135437p:plain:w500

これでMaskもできるテキストの形のレイヤーになりました。

f:id:sakura_bird1:20190804135731p:plain:w500

このレイヤーの上に画像を重ねて、テキストのレイヤーの右クリックメニューからMaskを選択すると
テキストの形に画像がマスクされます。

画像を動かすとマスクされる位置も変えられます。

f:id:sakura_bird1:20190804141142p:plain:w500

以上です。