0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NewsPicksAdvent Calendar 2024

Day 22

AndroidViewの思い出深いView3選

Last updated at Posted at 2024-12-21

こちらの記事は NewsPicks Advent Calendar 2024 の22日目の記事です。

こんにちは。NewsPicks Androidアプリエンジニアの sefwgweo です。

はじめに

今回は、タイトルにもある通りAndroidで様々なViewを作成してきましたが特に思い出深かったものをコードとともに紹介していこうと思います。
今更ComposeではなくAndroidView?と思われる方も多いかと思いますが、仕様を実現するために頭をひねらすという行為は決して無駄にはならないと思って記事を書いてみました。

構成はよくあるTabLayout + ViewPager2 + RecyclerViewとなっています。

また、今回もいつものようにコードを以下に載せてあります。
https://github.com/sefwgweo/CustomDesignTabAndMoreLayout

目次

  1. 環境構築
  2. 見た目をフルカスタマイズ可能なタブレイアウトの作り方
  3. 横スクロール時、コンテンツが滑らかに拡大表示するViewの作り方
  4. 透明Activityの作り方と活用方法

環境構築

前提として、今回のお話は全てComposeではなくAndroidViewとなります。
また、毎年恒例でこの記事のために新規でAndroidStudioのプロジェクトを作成して、イチからコードを書くようにしています。

では本題の環境構築に入ります。
今回はAndroid Studio Ladybug | 2024.2.1をMacOSのsequoiaで使っていますが、いつの日からかAndroidStudioではComposeで作ることが標準となっていたため、AndroidViewを使ってビルドするためには少しだけ手間をかける必要がありました。

具体的には以下2点に留意する必要がありましたが、無事ここまでで環境構築はOKです

  1. AndroidViewに関するライブラリを追加する(build.gradle.kts)
    • 具体的には androidx-recyclerview androidx-constraintlayout androidx-viewpager2 androidx-appcompat androidx-activity-ktx androidx-fragment-ktxのあたりになります
  2. 上記ライブラリに依存するライブラリのバージョン調整をする(libs.versions.toml)

また、Recyclerviewに関しては許可をいただき以下を使っています。
https://qiita.com/ko2ic/items/cf77f8f73986270dc3b0

見た目をフルカスタマイズ可能なタブレイアウトの作り方

タブのカスタムについては以前書いたAndroidのTabLayoutをカラフルにカスタマイズしてみたというものがありますが、今回と異なる点としては、添付画像にあるようにタブ内に画像を入れたりインジケーターの色や背景色をタブ毎に指定する箇所が以前と異なっているところにあります。
また、長らくタブレイアウトというのはOSの領域だった記憶があり、あまり見た目に関しては自由度が高くなかったという印象が強かったため今回選出しました。

実装時のポイントとしては以下のクラスでタブ用のTabItemViewModelを生成している箇所になります。以下な実装にすることで、ViewBindingを用いて色を変えたり画像を出したりが容易に実現出来ています。
TabFragment.kt

private fun initUIComponent() {
    binding.toolbar.title = getString(R.string.app_name)
        
    tabList = viewModel.fetchTabs()
        
    // viewModelから取得したタブ情報のリストを元にtabItemViewModelを生成
    val tabItemViewModel = tabList.map {
        TabItemViewModel(it)
    }
    val pagerAdapter = TabPagerAdapter(this@TabFragment, tabList)
    binding.pager.apply {
        adapter = pagerAdapter
        offscreenPageLimit = pagerAdapter.itemCount
        registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                tabItemViewModel.forEachIndexed { index, viewModel ->
                    binding.tabLayout.setSelectedTabIndicatorColor(
                        tabItemViewModel[position].indicatorColor
                    )
                }
            }
        })
    }
    TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position ->
        val binding = TabItemBinding.inflate(layoutInflater)

        // tabItemViewModelとXML(tab_item.xmlを紐づけ)
        binding.viewModel = tabItemViewModel[position]
        tab.customView = binding.root
    }.attach()
}

横スクロール時、コンテンツが滑らかに拡大表示するViewの作り方

動作イメージとしては以下な感じとなります。
Videotogif.gif

滑らかに拡大しているように見せるため、ガウススケールファクターを使用して実現しています。
実装では以下でガウススケール計算を行っています
BindingAdapters.kt

private fun getGaussianScale(
    view: RecyclerView,
    childCenterX: Int,
    minScaleOffset: Float,
    scaleFactor: Float,
    spreadFactor: Double,
): Float {
    val recyclerCenterX = (view.left + view.right) / 2
    return (
        Math.E.pow(
            -(childCenterX -recyclerCenterX.toDouble())
            .pow(2.toDouble()) / (2 * spreadFactor.pow(2.toDouble())
        )
    ) * scaleFactor * 0.05 + minScaleOffset).toFloat()
}

透明Activityの作り方と活用方法

動作イメージとしては以下な感じとなります。
Videotogif (1).gif

こちらは、以前書いた状況に応じて透明ActivityなThemeにOverrideする方法を実際に実装してみたものになります。

透明にしている箇所は以下です
DetailActivity

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    // 以下は透明なActivityにするための処理(OS11以上)
    window.setBackgroundDrawableResource(android.R.color.transparent)
    lifecycleScope.launch(Dispatchers.IO) {
        // メインスレッドで実行するとANRになるため、IOスレッドで実行
        setTranslucent(true)
    }
 }

下スワイプで後ろにあるページが見られるようにしているのが以下の実装となります。

DetailActivity

private fun initSwipeUI(binding: ActivityDetailBinding) {
    val param: CoordinatorLayout.LayoutParams = binding.container.layoutParams as CoordinatorLayout.LayoutParams
    param.behavior = BottomSheetBehavior<FrameLayout>()

    BottomSheetBehavior.from(binding.container).apply {
        state = BottomSheetBehavior.STATE_EXPANDED
        isHideable = true
        addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
            override fun onStateChanged(bottomSheet: View, newState: Int) {
                when (newState) {
                    BottomSheetBehavior.STATE_COLLAPSED -> {
                        state = BottomSheetBehavior.STATE_HIDDEN
                        finish()
                        overridePendingTransition(0, 0)
                    }

                    BottomSheetBehavior.STATE_HIDDEN -> {
                        return
                    }

                    else -> {
                        // noop
                    }
                }
            }

            override fun onSlide(bottomSheet: View, slideOffset: Float) {
                val upperThreshold = 0.7f
                if (state == BottomSheetBehavior.STATE_SETTLING && slideOffset < 1) {
                    if (slideOffset >= upperThreshold) {
                        // 位置が0.7以上の場合は、EXPAND状態にする
                        state = BottomSheetBehavior.STATE_EXPANDED
                    }
                }
            }
        })
    }
}

BottomSheetDialogFragmentを使っても同様の事は構成によっては可能ですが、DialogFragmentとしての縛りだったり、弊社のように原則1ページ1Activity1Fragment構成のケースも考慮した実装となっています。

おわりに

今回も結構マニアックなケースについて取り上げてみましたがいかがだったでしょうか

今後はAndroidViewに関して基本的には増えることはなく、順次Composeで書き直していくことになります。
冒頭でも触れた通り、仕様を満たすために様々な実験・調査・検証等をへて、その中で可能な限りベストな方法を選択する、というプロセスはそれがComposeだろうとそうでなかろうと大きく変わる事はないと思っていますので、そのあたりが参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?