#はじめに
Android用アプリのプログラミングを学ぶため、Googleが公開しているサンプルコード **Universal Android Music Player(UAMP)**をカスタマイズしてみます。
メニューをカスタマイズするために、オリジナルのコードの動作を確認します。
今回は、メニュー画面で項目がタップされてから下位のメニューを表示するまでの動作を確認します。
#環境
####PC
MacBook Pro 16
2.3 GHz 8コアIntel Core i9
16 GB 2667 MHz DDR4
macOS Big Sur ver.11.5.2
####開発用SW
Android Studio 4.2.1
####Target Device (Virtual Device)
Category: Phone
Name: Pixcel 2
Resolution: 1080x1920 420bpi
API Level: 28
Android: 9.0
CPU: x86
#コードの確認
###メニュー項目の選択(下位のメニューが存在する場合)
メニュー画面で項目がタップされてから下位のメニューを表示するまでの流れは下図のとおり。
(図が細かくなってしまいました。拡大してご確認ください。)
コードの関連する部分は下記。
なお、下記のコードは、音源を端末内のローカルファイルに変更済みのもの。
class MainActivity : AppCompatActivity() {
//途中省略
//mediaIdで指定されたTagのFragmentの存在を確認し、なければ生成して表示する関数。
private fun navigateToMediaItem(mediaId: String) {
//mediaIdで指定されたTagのFragmentを探す。
var fragment: MediaItemFragment? = getBrowseFragment(mediaId)
//mediaIdで指定されたTagのFragmentが存在しなければ生成する。
if (fragment == null) {
//次の画面のFragmentインスタンスを生成する。
fragment = MediaItemFragment.newInstance(mediaId)
//Fragmentの変更を要求する。
viewModel.showFragment(fragment, !isRootId(mediaId), mediaId)
}
}
//途中省略
//mediaIdで指定されたTagのFragmentを探し、結果をMediaItemFragmentにcastして返す関数。
private fun getBrowseFragment(mediaId: String): MediaItemFragment? {
return supportFragmentManager.findFragmentByTag(mediaId) as MediaItemFragment?
}
//途中省略
//オリジナルコードのonCreate()で実施していた処理を行うために作成した関数。
//アクティビティーを生成する。
private fun activityCreationProcess():Unit{
//途中省略
//メニュー変更イベント(MainActivityViewModelのnavigateToMediaItem)を監視。
//変化があればFragmentを更新する。
//navigationToMediaItem()の処理で、Fragment変更指示(navigateToFragment)が変更され、
//上で設定したobserverがこれを検知してFragmentの更新処理を行う。
viewModel.navigateToMediaItem.observe(this, Observer {
it?.getContentIfNotHandled()?.let { mediaId ->
navigateToMediaItem(mediaId)
}
})
}
//以下省略
class MainActivityViewModel(
private val musicServiceConnection: MusicServiceConnection
) : ViewModel() {
//途中省略
//メニュー更新指示イベント。
val navigateToMediaItem: LiveData<Event<String>> get() = _navigateToMediaItem
private val _navigateToMediaItem = MutableLiveData<Event<String>>()
//途中省略
//Fragment更新指示イベント。
val navigateToFragment: LiveData<Event<FragmentNavigationRequest>> get() = _navigateToFragment
private val _navigateToFragment = MutableLiveData<Event<FragmentNavigationRequest>>()
//途中省略
//Fragmentの更新を要求する関数。この関数が呼び出されると、Fragment更新指示イベントを発生する。
//イベントは、getContentIfNotHandled()で一度だけ読み出すことが可能。
fun showFragment(fragment: Fragment, backStack: Boolean = true, tag: String? = null) {
_navigateToFragment.value = Event(FragmentNavigationRequest(fragment, backStack, tag))
}
//途中省略
//タップされたメニュー項目に応じた処理を行う関数
fun mediaItemClicked(clickedItem: MediaItemData) {
if (clickedItem.browsable) {
//タップされた項目がbrowsable(階層化されたコンテンツリストで下位のコンテンツリストが存在する)であればメニュー変更イベント(navigateToMediaItem)を発生。
//メニュー変更イベントは、メニュー表示更新のトリガーとなる。
browseToItem(clickedItem)
} else {
//タップされた項目がbrowsableでなければ、選択されたコンテンツ(楽曲)を再生し、画面を再生画面に切り替える。
playMedia(clickedItem, pauseAllowed = false)
showFragment(NowPlayingFragment.newInstance())
}
}
//途中省略
//メニュー変更イベントを発生する関数。
private fun browseToItem(mediaItem: MediaItemData) {
_navigateToMediaItem.value = Event(mediaItem.mediaId)
}
//以下省略
class MediaItemFragment : Fragment() {
//途中省略
private lateinit var mediaId: String
private lateinit var binding: FragmentMediaitemListBinding
//リストアダプターを設定
//MediaItemAdapterの引数 "itemClickedListener"をラムダ式で設定。
//タップされたメニュー項目のMediaItemDataをmainActivityVMの関数mediaItemClicked()に引数として渡して実行する。
private val listAdapter = MediaItemAdapter { clickedItem ->
mainActivityViewModel.mediaItemClicked(clickedItem)
}
//途中省略
//ビューの生成
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMediaitemListBinding.inflate(inflater, container, false)
return binding.root
}
//新たなアクティビティが生成された際の処理
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mediaId = arguments?.getString(MEDIA_ID_ARG) ?: return
//メニュー項目リスト(mediaItemFragmentVMのmediaItems)の監視を設定。
mediaItemFragmentViewModel.mediaItems.observe(viewLifecycleOwner,
Observer { list ->
//loadSpinner(処理中を示すぐるぐる表示)の表示制御
binding.loadingSpinner.visibility =
//メニュー項目リストが空のときは、loadSpinnerを表示する。
if (list?.isNotEmpty() == true) View.GONE else View.VISIBLE
//メニュー項目を更新する
listAdapter.submitList(list)
})
//途中省略
//リサイクルビューにアダプターを設定する。
binding.list.adapter = listAdapter
}
}
class MediaItemAdapter(
//引数は関数型。MediaItemDataが引数として渡され、結果を返さない関数を引数とする。
private val itemClickedListener: (MediaItemData) -> Unit
) : ListAdapter<MediaItemData, MediaViewHolder>(MediaItemData.diffCallback) {
//表示するメニュー項目ごとにビューホルダーを生成する
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = FragmentMediaitemBinding.inflate(inflater, parent, false)
//メニュー項目がタップされたときに、関数型の引数itemClickedListenerが実行されるよう、ビューホルダーに設定。
return MediaViewHolder(binding, itemClickedListener)
}
//メニュー項目として表示データを設定する
override fun onBindViewHolder(
holder: MediaViewHolder,
position: Int,
payloads: MutableList<Any>
) {
//表示対象のメニュー項目の情報を取得する。
val mediaItem = getItem(position)
//アプリ起動、子メニュー選択などでメニュー画面を表示する際は、表示する全メニュー項目でpayloadはsize=0で、fullRefresh=Trueとなる。
//画面をスクロールして新たなメニュー項目を表示するときは、新たな項目をpayloadはsize=0、fullRefresh=Trueで表示する(既に表示されていたメニュー項目については、onBindingViewHolder()はcallされない)。
//画面をスクロールして、以前表示していた項目を再表示するときは、onBindingViewHolder()はcallされない。
//楽曲の再生が終了し、次の楽曲の再生を開始するときの再生状態(playbackState)の変化の際は、終了した楽曲と再生を開始する楽曲のメニュー項目の表示が、payloadのsize=1(isNotEmpty()=True)、payload=Trueで再生状態を示すアイコンのみ更新する。
var fullRefresh = payloads.isEmpty()
if (payloads.isNotEmpty()) {
payloads.forEach { payload ->
when (payload) {
PLAYBACK_RES_CHANGED -> {
holder.playbackState.setImageResource(mediaItem.playbackRes)
}
//payloadがsize != 0(isNotEmpty()=True)であっても、解読不能であればfullRefreshする。
else -> fullRefresh = true
}
}
}
//onBindingViewHolder()がcallされるときは、たいていfullRefresh。fullRefreshではないのは、再生状態を示すアイコンの変更時など。
if (fullRefresh) {
holder.item = mediaItem
holder.titleView.text = mediaItem.title
holder.subtitleView.text = mediaItem.subtitle
holder.playbackState.setImageResource(mediaItem.playbackRes)
Glide.with(holder.albumArt)
.load(mediaItem.albumArtUri)
.placeholder(R.drawable.default_art)
.into(holder.albumArt)
}
}
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
onBindViewHolder(holder, position, mutableListOf())
}
}
class MediaViewHolder(
binding: FragmentMediaitemBinding,
itemClickedListener: (MediaItemData) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
//メニュー画面タイトル行の設定
val titleView: TextView = binding.title
//メニュー画面サブタイトル行の設定
val subtitleView: TextView = binding.subtitle
//メニュー画面アルバム画像の設定
val albumArt: ImageView = binding.albumArt
//メニュー画面再生状態表示の設定
//楽曲メニュー画面でアルバム画像上に再生状態を示すアイコンを表示する。
val playbackState: ImageView = binding.itemState
var item: MediaItemData? = null
init {
//メニュー項目がタップされたときの動作を設定
binding.root.setOnClickListener {
//引数として与えられられた関数itemClickedListener()を実行する。
item?.let { itemClickedListener(it) }
}
}
}
class MusicServiceConnection(context: Context, serviceComponent: ComponentName) {
//途中省略
//mediaBrowserServiceに対して、mediaIdで指定した階層のコンテンツリストを要求
fun subscribe(parentId: String, callback: MediaBrowserCompat.SubscriptionCallback) {
mediaBrowser.subscribe(parentId, callback)
}
//途中省略
companion object {
@Volatile
private var instance: MusicServiceConnection? = null
//インスタンスを通知する関数
fun getInstance(context: Context, serviceComponent: ComponentName) =
instance ?: synchronized(this) {
instance ?: MusicServiceConnection(context, serviceComponent)
.also { instance = it }
}
}
}
//以下省略
open class MusicService : MediaBrowserServiceCompat() {
//途中省略
//階層化されたコンテンツリストの生成
private val browseTree: BrowseTree by lazy {
//変数が初めて使用される際に初期化される
BrowseTree(applicationContext, mediaSource)
//途中省略
//subscribe()への応答として、要求された階層のコンテンツのリストを返す
override fun onLoadChildren(
parentMediaId: String,
result: Result<List<MediaItem>>
) {
if (parentMediaId == UAMP_RECENT_ROOT) {
result.sendResult(storage.loadRecentSong()?.let { song -> listOf(song) })
} else {
val resultsSent = mediaSource.whenReady { successfullyInitialized ->
if (successfullyInitialized) {
//楽曲情報リストの取得が完了したら、submit()の応答として、要求された階層のコンテンツのリストを返す
val children = browseTree[parentMediaId]?.map { item ->
MediaItem(item.description, item.flag)
}
result.sendResult(children)
} else {
//楽曲情報リストの取得に失敗したら、nullを返す。
mediaSession.sendSessionEvent(NETWORK_FAILURE, null)
result.sendResult(null)
}
}
// 楽曲情報リストの取得に失敗したら、メッセージを現在のスレッドから切り離し、後でsendResult()を実行できるようにする。
if (!resultsSent) {
result.detach()
}
}
}
//以下省略
長くなってしまったので、ここで一旦切ります m(_ _)m
階層化されたコンテンツリストの生成、メニューのカスタマイズの実装などについては改めて投稿いたします。
#参考
Androidデベロッパー RecyclerView で動的リストを作成する
Androidデベロッパー RecyclerView の高度なカスタマイズ
2020年版RecyclerViewの使い方 〜 RecyclerView + ListAdapter + DataBinding + LiveData + ViewModel 〜
[Android]RecyclerView の ListAdapter を viewBinding と組み合わせて使う方法
RecyclerViewでListAdapterを使う
#関連する投稿#
Universal Android Music Player(UAMP)のカスタマイズ (音源の変更 1/2)
Universal Android Music Player(UAMP)のカスタマイズ (音源の変更 2/2)
Universal Android Music Player(UAMP)のカスタマイズ (メニューの変更 1)
#おわりに
プログラミング初心者、AndroidアプリのコードをいじるのもKotlinを使うのも初めてなので、誤りが多々あると思います。アドバイス、励ましのコメントなどいただけると嬉しいです。