Android Kotlin日本語チュートリアル-⑨ユーザビリティを向上させる
Android Kotlin日本語チュートリアル
本連載記事はこれからAndroidアプリ開発を始める人に向けたチュートリアルです。
コンセプトは
- プログラミングをあまり知らない人でも完走できる
- プログラミングにある程度詳しい人にも満足できる
- 実用的な知識を提供する
- とにかくわかりやすく
で、全9回と長めですが頑張っていきましょう。
このチュートリアルを終える頃には、Android開発の土台が形成されているだけでなくアプリケーションアーキテクチャの知識が出来上がっているはずです。
作成するのは以下のようなメモアプリです。
完成品は 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>
UIが多少おしゃれになりましたね。
今回はDividerだけの実装ですが、独自のItemDecorationを定義すれば、ItemView同士の間に文字列も描画することもできます。
アイコンをUIに挿入する
現在のUIでは、どれが更新日で、どれが締切日か分かりませんね。
フェイク文字列のように「締切: yyyy.MM.dd」とするのはあまり良い方法ではないので、何かしらアイコンを設けることにしましょう。
自前で画像ファイルを用意しても良いですが、今回はデフォルトで用意されているVector Asset Studioを利用してみます。
まずProjectウィンドウから「res」を右クリック →「new」から「Vector Asset」を選択してください。
そうすると以下のようなウィンドウが表示されます。
「Clip Art」を選択するとアイコン一覧が表示されますので、今回は試しに「Create」「」「」のアイコンを追加してみてください。
カラーコードは後で変えられるのでなんでも良いですがF6F6E9としておいてください。
「Name」はそれぞれ「create」「update」「delete」としておきます。
おそらくデフォルトで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>
これでユーザビリティもかなり向上しましたね。
Themeを変更してみる
現在のアプリはデフォルトのテーマを利用しており、ここもオリジナリティがありませんね。
そこで、テーマカラーを独自のものに置き換えてみましょう。
また、最近のスマートフォンはナイトモードも存在することがほとんどなので、そちらもテーマを別途用意する必要があります。
それぞれ、res/values/theme.xmlとres/values-night/theme.xmlで管理されます。
大体何色用意すれば良いのか、どのような色を用意すれば良いのかは、デフォルトのcolors.xmlに用意されているものを参考にすると良いです。
ここからは私の好みでテーマを構築しますが、この部分はみなさんのオリジナリティが出やすいところなので、私と同じにしなくても良いです。
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">
...
第9回のまとめ
- ItemDecorationを独自で用意するとRecyclerViewの各ItemViewに対して、図形や文字列を挿入できる
- Vector Asset Studioから、デフォルトで用意されているアイコンを
drawable/
リソースディレクトリにインポートできる - themes.xmlやcolors.xmlなどはダークモード専用のものを用意できる
おわりに
これでAndroid Kotlin日本語チュートリアルは全編終了となります!
これでAndroid開発の基礎、そしてソフトウェア開発の基礎はある程度身についていると思います。
とは言え、まだまだ学ぶべきAndroid開発知識はあるので、本チュートリアルを土台にしてさらに知識を深めていってほしいです。
ここからはこれから学ぶべき要素について羅列しておきますので、次のステップに進む際にぜひ参考にしてください。
またいつか本チュートリアルの続きとして記事を書くかもしれませんが、それまでは下記に公式ドキュメントリンクを載せておきます。
- Service
- サービスの概要 | Android developers
- UIを持たず、バックグラウンドで動作するアプリコンポーネント
- ソフトウェアキーボードを強制的に隠す実装をしましたが、あれもServiceの一種です
- Broadcast Receiver
- ブロードキャストの概要 | Android developers
- 1分ごとにイベントを通知したり、日を跨いだらイベントを通知したりと、Androidが送信するブロードキャストを受信して、その度に任意の処理を実行できる
- Notification
- 通知を作成する | Android developers
- 例えば、「メモの締め切り(破棄日)が到達したらアプリ起動しているかいないかに関わらずユーザに通知をしたい」というユースケースがあればこれを使う
- テスト
- 【番外編】Android Kotlin日本語チュートリアル – 単体テストを書いてみる
- ☝︎記事を書きました! (2021/12/10)