araki tech

for developers including me

Android Kotlin日本語チュートリアル-⑨ユーザビリティを向上させる

Android Kotlin日本語チュートリアル

Android Kotlin日本語チュートリアル

本連載記事はこれからAndroidアプリ開発を始める人に向けたチュートリアルです。

コンセプトは

  • プログラミングをあまり知らない人でも完走できる
  • プログラミングにある程度詳しい人にも満足できる
  • 実用的な知識を提供する
  • とにかくわかりやすく

で、全9回と長めですが頑張っていきましょう。

このチュートリアルを終える頃には、Android開発の土台が形成されているだけでなくアプリケーションアーキテクチャの知識が出来上がっているはずです。

作成するのは以下のようなメモアプリです。

Android Kotlin日本語チュートリアル

完成品は HiroshiARAKI/AndroidKotlinTutrialで公開していますので適宜参考にしてください。

第9回 : ユーザビリティを向上させる

第9回は、ユーザビリティを向上させるべく、UIを細かく整備していきましょう。

現在のアプリはAndroidデフォルトのUIデザインで、あまりオリジナリティがありません。

そこで今回は、ItemDecorationを独自に用意して、少しだけデザインを凝ってみましょう。

ItemDecoration

ItemDecorationは、RecyclerViewの各ViewHolderのUIを簡単にいじることができるクラスです。

現在はデフォルトで用意されているものを使用しています。

recyclerView.addItemDecoration(
    DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)

これを独自で用意して、RecyclerViewのデザインを整備しましょう。

新たにtool/ディレクトリを用意して、MemoItemDecoration.ktを作成してください。

中身は以下のようにしてみます。

class MemoItemDecoration : RecyclerView.ItemDecoration() {
    companion object {
        private val OFFSET_TOP_WIDTH = 5.dp
        private val BORDER_COLOR = Paint().apply {
            color = Color.rgb(0xd7, 0xee, 0xf2)
        }
    }

    // 各Viewのオフセットを管理する
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        val position = parent.getChildAdapterPosition(view)
        // 最初のViewHolderを除き、上部にオフセットを設ける
        if (position != 0)
            outRect.top = OFFSET_TOP_WIDTH
    }

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        repeat(state.itemCount) { position ->
            val view = parent.getChildAt(position) ?: return  // 描画範囲外であればreturn

            // 作成したオフセットに長方形を描画してDividerを作成
            c.drawRect(
                0f,  // left
                view.y - OFFSET_TOP_WIDTH,  // top
                parent.width.toFloat(),  // right
                view.y,  // bottom
                BORDER_COLOR  // background color
            )
        }
    }
}

各処理はコメントを添えています。

さらに、ViewHolderの背景色を以下のように変えれば…

memo_item_view.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:padding="10dp"
    android:background="@color/main_color">

    <TextView
        android:id="@+id/memo_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        android:text="Sample Memo Title"
        android:textColor="@color/sub_color"
        app:layout_constraintTop_toTopOf="parent"
        android:padding="5dp"/>

    <TextView
        android:id="@+id/memo_created_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"
        android:text="作成: 2020.01.01"
        android:textColor="@color/sub_color"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintEnd_toStartOf="@id/memo_updated_date"
        android:padding="5dp"/>

    <TextView
        android:id="@+id/memo_updated_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"
        android:text="更新: 2020.01.01"
        android:textColor="@color/sub_color"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintEnd_toStartOf="@id/memo_expired_date"
        android:padding="5dp"/>

    <TextView
        android:id="@+id/memo_expired_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"
        android:text="締切: 2020.01.01"
        android:textColor="@color/sub_color"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintEnd_toEndOf="parent"
        android:padding="5dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 省略 -->

    <color name="main_color">#194769</color>
    <color name="sub_color">#F6F6E9</color>

    <!-- 省略 -->
</resources>

Android Kotlin日本語チュートリアル

UIが多少おしゃれになりましたね。

今回はDividerだけの実装ですが、独自のItemDecorationを定義すれば、ItemView同士の間に文字列も描画することもできます

アイコンをUIに挿入する

現在のUIでは、どれが更新日で、どれが締切日か分かりませんね。

フェイク文字列のように「締切: yyyy.MM.dd」とするのはあまり良い方法ではないので、何かしらアイコンを設けることにしましょう。

自前で画像ファイルを用意しても良いですが、今回はデフォルトで用意されているVector Asset Studioを利用してみます。

まずProjectウィンドウから「res」を右クリック →「new」から「Vector Asset」を選択してください。

Android Kotlin日本語チュートリアル

そうすると以下のようなウィンドウが表示されます。

Android Kotlin日本語チュートリアル

「Clip Art」を選択するとアイコン一覧が表示されますので、今回は試しに「Create」「」「」のアイコンを追加してみてください。

カラーコードは後で変えられるのでなんでも良いですがF6F6E9としておいてください。

「Name」はそれぞれ「create」「update」「delete」としておきます。

Android Kotlin日本語チュートリアル

おそらくデフォルトでdrawable/ディレクトリに配置されると思いますが、基本アプリアイコン含む画像データはここに配置されます。

そうしたらレイアウトファイルに配置してみましょう。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:padding="10dp"
    android:background="@color/main_color">

    <TextView
        android:id="@+id/memo_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        android:text="Sample Memo Title"
        android:textColor="@color/sub_color"
        app:layout_constraintTop_toTopOf="parent"
        android:padding="5dp"/>

    <ImageView
        android:id="@+id/crate_icon"
        android:layout_width="15sp"
        android:layout_height="15sp"
        android:background="@drawable/create"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/memo_created_date" />

    <ImageView
        android:id="@+id/update_icon"
        android:layout_width="15sp"
        android:layout_height="15sp"
        android:background="@drawable/update"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/memo_created_date"
        app:layout_constraintEnd_toStartOf="@id/memo_updated_date" />

    <ImageView
        android:id="@+id/delete_icon"
        android:layout_width="15sp"
        android:layout_height="15sp"
        android:background="@drawable/delete"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/memo_updated_date"
        app:layout_constraintEnd_toStartOf="@id/memo_expired_date" />

    <TextView
        android:id="@+id/memo_created_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"
        android:text="2020.01.01"
        android:textColor="@color/sub_color"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintEnd_toStartOf="@id/update_icon"
        app:layout_constraintStart_toEndOf="@id/crate_icon"
        android:padding="5dp"/>

    <TextView
        android:id="@+id/memo_updated_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"
        android:text="2020.01.01"
        android:textColor="@color/sub_color"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintStart_toEndOf="@id/update_icon"
        app:layout_constraintEnd_toStartOf="@id/delete_icon"
        android:padding="5dp"/>

    <TextView
        android:id="@+id/memo_expired_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"
        android:text="2020.01.01"
        android:textColor="@color/sub_color"
        app:layout_constraintTop_toBottomOf="@id/memo_title"
        app:layout_constraintStart_toEndOf="@id/delete_icon"
        app:layout_constraintEnd_toEndOf="parent"
        android:padding="5dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Android Kotlin日本語チュートリアル

これでユーザビリティもかなり向上しましたね。




Themeを変更してみる

現在のアプリはデフォルトのテーマを利用しており、ここもオリジナリティがありませんね。

そこで、テーマカラーを独自のものに置き換えてみましょう。

また、最近のスマートフォンはナイトモードも存在することがほとんどなので、そちらもテーマを別途用意する必要があります。

それぞれ、res/values/theme.xmlとres/values-night/theme.xmlで管理されます。

大体何色用意すれば良いのか、どのような色を用意すれば良いのかは、デフォルトのcolors.xmlに用意されているものを参考にすると良いです。

Android Kotlin日本語チュートリアル

ここからは私の好みでテーマを構築しますが、この部分はみなさんのオリジナリティが出やすいところなので、私と同じにしなくても良いです。

res/values/colors.xml
<resources>
    <color name="deep_blue">#194769</color>
    <color name="normal_blue">#335975</color>
    <color name="light_blue">#7396AF</color>
    <color name="orange">#C24F4F</color>
    <color name="light_orange">#BD786C</color>
    <color name="black">#111111</color>
    <color name="white">#F6F6E9</color>
    <color name="dark_gray">#333333</color>
    <color name="light_gray">#F0F0F0</color>

    <color name="main_color">@color/deep_blue</color>
    <color name="sub_color">@color/white</color>
    
    <color name="list_border_color">@color/light_blue</color>

    <color name="memo_fragment_background">@color/white</color>
    <color name="memo_fragment_contents_background">@color/light_gray</color>

    <color name="delete">@color/orange</color>
    <color name="datetime_picker_background">#55194769</color>
</resources>
res/values-night/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="main_color">@color/deep_blue</color>
    <color name="sub_color">@color/white</color>

    <color name="list_border_color">@color/black</color>

    <color name="memo_fragment_background">@color/black</color>
    <color name="memo_fragment_contents_background">@color/dark_gray</color>
</resources>
res/values/themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.SmartMemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/normal_blue</item>
        <item name="colorPrimaryVariant">@color/deep_blue</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/light_orange</item>
        <item name="colorSecondaryVariant">@color/orange</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>
res/values-night/themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.SmartMemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/light_blue</item>
        <item name="colorPrimaryVariant">@color/deep_blue</item>
        <item name="colorOnPrimary">@color/black</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/light_orange</item>
        <item name="colorSecondaryVariant">@color/light_orange</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>
MemoItemDecoration.kt
class MemoItemDecoration : RecyclerView.ItemDecoration() {
    companion object {
        private val OFFSET_TOP_WIDTH = 5.dp
    }

    // ... 

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        repeat(state.itemCount) { position ->
            val view = parent.getChildAt(position) ?: return  // 描画範囲外であればreturn

            // 作成したオフセットに長方形を描画してDividerを作成
            val color = Paint().apply {
                color = ContextCompat.getColor(view.context, R.color.list_border_color)
            }
            c.drawRect(
                0f,  // left
                view.y - OFFSET_TOP_WIDTH,  // top
                parent.width.toFloat(),  // right
                view.y,  // bottom
                color  // background color
            )
        }
    }
}

あとはfragment_memo_detailとfragment_new_memoの背景色を以下のように変えてみました。

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/memo_fragment_background"
    android:clickable="true">
 
    ...
Android Kotlin日本語チュートリアル
レイアウトプレビューでもダークモードを選択可能です

Android Kotlin日本語チュートリアル

第9回のまとめ

  • ItemDecorationを独自で用意するとRecyclerViewの各ItemViewに対して、図形や文字列を挿入できる
  • Vector Asset Studioから、デフォルトで用意されているアイコンをdrawable/リソースディレクトリにインポートできる
  • themes.xmlやcolors.xmlなどはダークモード専用のものを用意できる

おわりに

これでAndroid Kotlin日本語チュートリアルは全編終了となります!

これでAndroid開発の基礎、そしてソフトウェア開発の基礎はある程度身についていると思います。

とは言え、まだまだ学ぶべきAndroid開発知識はあるので、本チュートリアルを土台にしてさらに知識を深めていってほしいです。

ここからはこれから学ぶべき要素について羅列しておきますので、次のステップに進む際にぜひ参考にしてください。

またいつか本チュートリアルの続きとして記事を書くかもしれませんが、それまでは下記に公式ドキュメントリンクを載せておきます。

参考