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>