在各App中, 经常会看到 “Tab栏加ViewPage” 的样式,ViewPage每切换一页, Tab栏就自动选中对应Tab项, 也可以点击Tab项来切换ViewPage的页面,当Tab内容少, 不足屏幕宽度时, 可以不用处理, 但当Tab项很多, 一个屏幕宽度显示不下时, 就需要Tab栏能够滚动, 当选中一个Tab项时,需要把当前选中的Tab项移动到中间位置, 在滑动ViewPage时需要Tab栏联动滚动。
例如 简书App:
![](https://img.haomeiwen.com/i20398968/e3ce3638b5c0a131.gif)
可以看到, 在滑动下面的Page时, 小Tab栏(综合、最新、读书....)自动选中, 并且在滑到对应Tab项 超过tab栏中间位置时, Tab栏 会自动滚动,把选中的Tab项居中显示,那这是怎么实现的呢。
先分析下过程:
1.Tab栏的滚动是由ViewPage触发, 那么肯定要监听ViewPage的滚动 。
2.既然Tab栏可以横向滚动, 那可以有几种实现方式: RecyclerView、HorizontalScrollView、自定义ViewGroup , 从这三个中挑个最简单的 HorizontalScrollView, 每个Tab项都是HorizontalScrollView中的子View, 可以任意类型, 图片、文字、View...都可。
- 最后再根据ViewPage滑动的位置, 计算Tab栏需要滚动的距离, 调用ScrollView的scrollTo进行滚动即可。
过程理完了, 接下来就是实现验证了。
先看布局 HorizontalScrollView + ViewPage2
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<HorizontalScrollView
android:id="@+id/hs_title"
android:layout_width="match_parent"
android:layout_height="40dp"
android:scrollbars="none"
>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"/>
</HorizontalScrollView>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.appcompat.widget.LinearLayoutCompat>
具体实现
- 监听ViewPage滚动:
viewPage.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
}
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
}
})
}
- 给 ScrollView 添加子View
private fun createTitle(
ll: LinearLayoutCompat,
list: ArrayList<Fragment>,
hs: HorizontalScrollView,
vp: ViewPager2
) {
list.forEachIndexed { index, fragment ->
val textView = AppCompatTextView(this)
textView.text = "我是标题 $index"
textView.textSize = 20f
val lp = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.WRAP_CONTENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT)
lp.marginEnd = 40
textView.layoutParams = lp
ll.addView(textView)
textView.setOnClickListener {
vp.setCurrentItem(index, true)
}
}
changeSelectItem(0, ll, hs)
}
ll表示的是ScrollView中的唯一子View, 因为ScrollView只允许有一个子View, 所以我们把Tab项全部加入这个ll中, 包括设置样式、点击事件(用来切换ViewPage)等等。
- 监听ViewPage滚动距离,设置 ScrollView 滚动位置
ViewPage 的滚动监听有三个方法, 当我们手指触摸到ViewPage准备开始滑动时时会调用 onPageScrollStateChanged(),
此时 先记录 当前ScrollView 的滚动位置, 以及当前的ViewPage页下标:
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
if(state == SCROLL_STATE_DRAGGING){
mViewPagerIndex = viewPage.currentItem
currentScrollX = hs.scrollX
}
}
在开始滑动时会调用 onPageScrolled(),这个方法会回调页面的滑动进度,在这个方法中判断是左滑还是右滑, 如果是左滑, 则滑动进度 position (0 ~ 1), 然后获取下一个Tab项距离Tab栏中间的距离, 用距离 乘 进度 就可以实现 Tab栏随ViewPage滚动而滚动了:
例如:
Tab栏宽度为1080, 下一个项的x 为 600, 那么它需要往左移动的距离为 moveX = 600 - (1080 / 2)- (width / 2), (width / 2)是这个Tab项自己宽度的一半, 这样就可以实现Tab项居中了;
当position 由 0 ~ 1 变化时, ScrollView 往左滑动的距离就由 当前滚动位置 + 0 ~ moveX 变化。
看看具体实现:
//正在向左滑动
if(mViewPagerIndex == position){
val realPosition = position + 1
if (realPosition >= list.size) {
return
}
if (positionOffset <= 0 || positionOffset >= 1) {
return
}
val child = ll.getChildAt(realPosition)
val childWidth = child.width
val scrollWidth = hs.width
val scrollX = child.left - scrollWidth / 2 + (childWidth / 2)
val inScrollX = scrollX - currentScrollX
val sX = currentScrollX + inScrollX * positionOffset
hs.scrollTo(sX.toInt(), 0)
//正在向右滑动
}else if(mViewPagerIndex == position + 1){
if (positionOffset <= 0 || positionOffset >= 1) {
return
}
val child = ll.getChildAt(position)
val childWidth = child.width
val scrollWidth = hs.width
val scrollX = child.left - scrollWidth / 2 + (childWidth / 2)
val inScrollX = scrollX - currentScrollX
val sX = currentScrollX + (inScrollX - inScrollX * positionOffset)
hs.scrollTo(sX.toInt(), 0)
}
当当前ViewPage切换页面完成时, 会回调 onPageSelected (), 此时我们为Tab选中项切换选中样式, 就完成了:
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
changeSelectItem(position, ll, hs)
}
private fun changeSelectItem(position: Int, ll: LinearLayoutCompat, hs: HorizontalScrollView) {
for (i in 0 until ll.childCount) {
val child = ll.getChildAt(i) as AppCompatTextView
child.setTextColor(ContextCompat.getColor(this, R.color.black))
}
val child = ll.getChildAt(position) as AppCompatTextView
child.setTextColor(ContextCompat.getColor(this, android.R.color.holo_red_dark))
val childWidth = child.width
val scrollWidth = hs.width
val scrollX = child.left - scrollWidth / 2 + (childWidth / 2)
hs.scrollTo(scrollX, 0)
}
大致思路就是如此, 一些样式的设置、滚动条的添加、子View不同类型的设置, 都可此基础上修改,。
贴上全部代码:
class MainActivity : AppCompatActivity() {
private var mViewPagerIndex: Int = -1
private var currentScrollX = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewPage = findViewById<ViewPager2>(R.id.vp)
val list = createFragment()
viewPage.adapter = FragmentViewPageAdapter(this, list)
val hs = findViewById<HorizontalScrollView>(R.id.hs_title)
val ll = findViewById<LinearLayoutCompat>(R.id.ll_title)
createTitle(ll, list, hs, viewPage)
viewPage.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
if(state == SCROLL_STATE_DRAGGING){
mViewPagerIndex = viewPage.currentItem
currentScrollX = hs.scrollX
}
if(state == SCROLL_STATE_IDLE) {
mViewPagerIndex = -1
currentScrollX = -1
}
}
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
if (mViewPagerIndex < 0 || currentScrollX < 0){
return
}
//正在向左滑动
if(mViewPagerIndex == position){
val realPosition = position + 1
if (realPosition >= list.size) {
return
}
if (positionOffset <= 0 || positionOffset >= 1) {
return
}
val child = ll.getChildAt(realPosition)
val childWidth = child.width
val scrollWidth = hs.width
val scrollX = child.left - scrollWidth / 2 + (childWidth / 2)
val inScrollX = scrollX - currentScrollX
val sX = currentScrollX + inScrollX * positionOffset
hs.scrollTo(sX.toInt(), 0)
//正在向右滑动
}else if(mViewPagerIndex == position + 1){
if (positionOffset <= 0 || positionOffset >= 1) {
return
}
val child = ll.getChildAt(position)
val childWidth = child.width
val scrollWidth = hs.width
val scrollX = child.left - scrollWidth / 2 + (childWidth / 2)
val inScrollX = scrollX - currentScrollX
val sX = currentScrollX + (inScrollX - inScrollX * positionOffset)
hs.scrollTo(sX.toInt(), 0)
}
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
changeSelectItem(position, ll, hs)
}
})
}
private fun createFragment(): java.util.ArrayList<Fragment> {
val list = ArrayList<Fragment>()
for (i in 0..10) {
val ifra = ItemFragment()
list.add(ifra)
}
return list
}
private fun createTitle(
ll: LinearLayoutCompat,
list: ArrayList<Fragment>,
hs: HorizontalScrollView,
vp: ViewPager2
) {
list.forEachIndexed { index, fragment ->
val textView = AppCompatTextView(this)
textView.text = "我是标题 $index"
textView.textSize = 20f
val lp = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.WRAP_CONTENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT)
lp.marginEnd = 40
textView.layoutParams = lp
ll.addView(textView)
textView.setOnClickListener {
vp.setCurrentItem(index, true)
}
}
changeSelectItem(0, ll, hs)
}
private fun changeSelectItem(position: Int, ll: LinearLayoutCompat, hs: HorizontalScrollView) {
for (i in 0 until ll.childCount) {
val child = ll.getChildAt(i) as AppCompatTextView
child.setTextColor(ContextCompat.getColor(this, R.color.black))
}
val child = ll.getChildAt(position) as AppCompatTextView
child.setTextColor(ContextCompat.getColor(this, android.R.color.holo_red_dark))
val childWidth = child.width
val scrollWidth = hs.width
val scrollX = child.left - scrollWidth / 2 + (childWidth / 2)
hs.scrollTo(scrollX, 0)
}
}
最后看看效果:
![](https://img.haomeiwen.com/i20398968/43b1ea07b1fe6919.gif)
结束...
网友评论