1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LinerLayoutを拡張してマーカー付きタブレイアウトを実装する

Last updated at Posted at 2024-12-13

AndroidではMaterialComponentsでTabLayoutが提供されています、ViewPagerと組み合わせてタブを提供するなどを簡単に実装することができます。
ただ、このTabLayout、そのまま素直につかうのなら簡単なのですが、少しデザインを凝ったり、タブに機能を追加しようとすると途端に難易度が上がります。カスタマイズ性は高いものの、内部実装の都合で多くの制約があるため、一定以上のカスタマイズが必要な場合は、いっそ一から自前で作ってしまった方が楽だったりします。
その場合、おそらくすぐに実装方法が思いつかなさそうなのが、マーカーかな?ということでシンプルな実装方法を紹介します。
実装したものが以下です。

LinearLayoutを拡張

同一構造の要素が並んだViewなのでRecyclerViewが思い浮かぶ火らもしれませんが、RecyclerViewの場合、スクロールの制御を細かくやろうとしたり、表示される前の要素の位置や大きさを知る必要があると、難易度が高いです。たかだか数個程度のタブであれば、LienarLayout/HorizontalScrollViewあたりを利用するのが簡単です。

単純にフォーカスを当てる場所を指定すればそこにマーカーを表示し、変化があれば移動アニメーションをおこなうという実装が以下になります。

class TabView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
) : LinearLayout(
    context, attrs, defStyleAttr
) {
    init {
        orientation = HORIZONTAL
    }

    private val marker: Drawable = AppCompatResources.getDrawable(context, R.drawable.marker)!!
    private var animator: ValueAnimator? = null
    private var currentIndex = -1

    fun setSelected(index: Int) {
        val current = children.elementAtOrNull(currentIndex)
        val next = children.elementAtOrNull(index)
        currentIndex = index
        next ?: return
        animator?.cancel()
        animator = null
        if (current == null) {
            marker.setBounds(next.left, next.bottom - marker.intrinsicHeight, next.right, next.bottom)
            invalidate()
            return
        }
        animator = ValueAnimator.ofFloat(0f, 1f).also {
            it.addUpdateListener {
                val fraction = it.animatedValue as Float
                val left = (current.left + (next.left - current.left) * fraction).toInt()
                val right = (current.right + (next.right - current.right) * fraction).toInt()
                marker.setBounds(left, next.bottom - marker.intrinsicHeight, right, next.bottom)
                invalidate()
            }
            it.start()
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        marker.draw(canvas)
    }
}

かなり割り切っているとはいえ、非常にシンプルに実装することができます。

なお、貼り付けたスクショの場合、markerに以下のようなGradientDrawableを使っています。

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle"
    >
    <solid android:color="?attr/colorPrimary" />

    <corners
        android:bottomLeftRadius="0dp"
        android:bottomRightRadius="0dp"
        android:topLeftRadius="4dp"
        android:topRightRadius="4dp"
        />
    <size android:height="4dp" />
</shape>
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?