ViewPager自动滚动
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ViewPager自动滚动相关的知识,希望对你有一定的参考价值。
参考技术A ViewPage 实现自动滚动并没有通过ScheduledExcutorService或者Timer定期执行某个任务实现,而是简单的通过handler发送消息去完成一次滚动,在完成一次滚动后发送另外一个delay的滚动消息,如此循环实现。ViewPager 滑动速度的设置是通过反射的方式重新设置ViewPager的Scroller,改变Scroller的startScroll的间隔时间完成的。调用setScrollDuration(double)即可。
如何使用呢?
首先,引入公共库。
android Auto Scroll ViewPager 作为你项目的library。
然后,调用。分两步。
第一步,利用 自动滚动 的AutoScrollViewPager来代替一般的ViewPager。
第二步,启动ViewPager自动滚动。
startAutoScroll()启动自动滚动
stopAutoScroll()停止自动滚动
另外,我们可以根据自己的需求进行一系列相关的设置。
setInterval(long)设置自动滚动的间隔时间,单位为毫秒
setDirection(int)设置自动滚动的方向,默认向右
setCycle(boolean)是否自动循环轮播,默认为ture
setScrollDurationFactor(double)设置VIewPager滑动动画间隔时间的倍率,达到减慢动画或者改变动画速度的效果
setStopScrollWhenTouch(boolean)当手指碰到ViewPager时是否停止自动滚动,默认为true。
setSlideBorderMote(int)滑动到第一个或者最后一个Item的处理方式,支持没有任何操作、轮播以及传递到父View三种模式。
setBorderAnimation(boolean)设置循环滚动时滑动到边缘滚动到下一个是否需要动画,默认为true。
Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东美团首页效果Tab页滚动到顶部时自动吸附
先上效果图(TabLayout滚动到顶部时自动吸附):
先看下布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
tools:context=".nestedscroll.NestedScrollActivity">
<com.zs.test.nestedscroll.ZSNestedScrollView
android:id="@+id/activity_nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/activity_nested_view"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@drawable/home_top_banner1" />
<LinearLayout
android:id="@+id/activity_nested_ll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/activity_nested_tb"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#ff7f00"
app:tabSelectedTextColor="#0000ff"
app:tabTextColor="#00ff00" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/activity_nested_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
</com.zs.test.nestedscroll.ZSNestedScrollView>
</LinearLayout>
其中 com.zs.test.nestedscroll.ZSNestedScrollView 是实现此效果的关键所在,其他都是常规的布局代码
package com.zs.test.nestedscroll
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.RecyclerView
import com.zs.test.util.log
@RequiresApi(Build.VERSION_CODES.M)
class ZSNestedScrollView : NestedScrollView
/**
* 顶部的view id = activity_nested_view
*/
private var topView: View? = null
/**
* 包裹TabLayout+RecyclerView 的 LinearLayout id = activity_nested_ll
*/
private var contentView: ViewGroup? = null
/**
* 处理惯性滑动的工具类
*/
private var mFlingHelper: FlingHelper? = null
/**
* 记录当前自身已经滑动的距离
*/
var totalDy = 0
/**
* 用于判断RecyclerView是否在fling
*/
var isRecyclerViewStartFling = false
/**
* 记录当前滑动的y轴加速度
*/
private var velocityY = 0
constructor(context: Context) : super(context)
init()
/**
* 必须的构造函数,系统会通过反射来调用此构造方法完成view的创建
*/
constructor(context: Context, attr: AttributeSet) : super(context, attr)
init()
constructor (context: Context, attr: AttributeSet, defZStyle: Int) : super(
context,
attr,
defZStyle
)
init()
private fun init()
mFlingHelper = FlingHelper(context)
//添加滚动监听 v就是当前NestedScrollLayout
setOnScrollChangeListener v, scrollX, scrollY, oldScrollX, oldScrollY ->
if (isRecyclerViewStartFling)
totalDy = 0
isRecyclerViewStartFling = false
//scrollY 是 当前向上滑动了多少 0 就是一点没滑动 就是在顶部状态
if (scrollY == 0)
log("TOP SCROLL")
// refreshLayout.setEnabled(true);
//v.measuredHeight() 就是屏幕高度
if (scrollY == topView!!.measuredHeight)
log("BOTTOM SCROLL v.getMeasuredHeight() = " + v.measuredHeight)
//滑动到底部以后 还有惯性让子类接着来滑动
dispatchChildFling()
//在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移
totalDy += scrollY - oldScrollY
private fun dispatchChildFling()
if (velocityY != 0)
//将惯性的加速度转换为具体的距离
val splineFlingDistance = mFlingHelper!!.getSplineFlingDistance(velocityY)
//举例解释:假设用力滑动一下 能滑动100个单位的距离,totalDy是外层ZSNestedScrollView已经滑动的距离
// 假设是50 那么还有50 咋办呢 ,要让子布局(RecycleView)来滑动剩下的50
if (splineFlingDistance > totalDy)
childFling(
mFlingHelper!!.getVelocityByDistance(
splineFlingDistance - java.lang.Double.valueOf(
totalDy.toDouble()
)
)
)
//重置变量
totalDy = 0
velocityY = 0
private fun childFling(velY: Int)
if (contentView != null)
val childRecyclerView: RecyclerView? = getChildRecyclerView(contentView!!)
childRecyclerView?.fling(0, velY)
private fun getChildRecyclerView(viewGroup: ViewGroup): RecyclerView?
for (i in 0 until viewGroup.childCount)
val view = viewGroup.getChildAt(i)
if (view is RecyclerView && view.javaClass == RecyclerView::class.java)
return viewGroup.getChildAt(i) as RecyclerView
else if (viewGroup.getChildAt(i) is ViewGroup)
val childRecyclerView: ViewGroup? =
getChildRecyclerView(viewGroup.getChildAt(i) as ViewGroup)
if (childRecyclerView is RecyclerView)
return childRecyclerView
continue
return null
override fun fling(velocityY: Int)
super.fling(velocityY)
if (velocityY <= 0)
this.velocityY = 0
else
isRecyclerViewStartFling = true
this.velocityY = velocityY
/**
* view 加载完成后执行
*/
override fun onFinishInflate()
super.onFinishInflate()
topView = (getChildAt(0) as ViewGroup).getChildAt(0)
contentView = (getChildAt(0) as ViewGroup).getChildAt(1) as ViewGroup
/**
* 参数 解释
* target 触发嵌套滑动的 view
* dx 表示 view 本次 x 方向的滚动的总距离,单位:像素
* dy 表示 view 本次 y 方向的滚动的总距离,单位:像素
* consumed 输出:表示父布局消费的水平和垂直距离。
* type 触发滑动事件的类型:其值有
* ViewCompat. TYPE_TOUCH
* ViewCompat. TYPE_NON_TOUCH
*
*/
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int)
// log(getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight())
// log( "dx="+ dx + " dy=" + dy)
// 如果能继续向上滑动,就滑动
val canScroll = dy > 0 && scrollY < topView!!.measuredHeight
if (canScroll)
scrollBy(0, dy)
consumed[1] = dy
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val lp = contentView!!.layoutParams
// getMeasuredHeight() 得到的就是屏幕高度 当前ZSNestedScrollView高度是MatchParent
log("onMeasure getMeasuredHeight() = $measuredHeight")
lp.height = measuredHeight
// 调整contentView的高度为屏幕高度,这样ZSNestedScrollView总高度就是屏幕高度+topView的高度
// 因此往上滑动 滑完topView后,TabLayout就卡在顶部了,因为ZSNestedScrollView滑不动了啊,就这么高
// 接着在滑就是其内部的RecyclerView去滑动了
contentView!!.layoutParams = lp
关键逻辑都加了注释,这里就简单总结下实现思路吧:
第一步:在RecyclerView区域滚动时 要实现能够整体滚动,而不是单独的RecyclerView自身在滚动,这个我们借助系统的力量就可以了,我们写一个ZSNestedScrollView 继承NestedScrollView即可。
第二步:实现TabLayout滚动到顶部时自动吸附在顶部保持不动,这里我们做一个巧妙的的设置:我们把TabLayout + ViewPager的总高度设置为屏幕的高度,这样ZSNestedScrollView总高度就是屏幕高度+topView的高度 ,因此往上滑动 滑完topView后,TabLayout就会呆在顶部了不动了,此时在让RecyclerView内部滑动就可以了,如下图所示:
第三步:第二步高度固定后,在RecyclerView区域滚动时,将不能再整体滚动,于是我们要手动处理,当在RecyclerView区域往上滑时,主动判断父布局ZSNestedScrollView是否可以滑动,是的话,让父类滑动:
这里dy表示view本次y方式的股东的总距离,单位像素,scrollY是ZSNestedScrollView Y轴上已经滑动的距离,小于topView的高度说明还有继续往上滑的空间
第四步:处理惯性滑动,用户在RecyclerView区域用力往上一滑的时候,TabLayout吸附到顶部以后,应该有继续滑动的效果来,这样更顺畅,体验更好,京东、美图都是这样实现的。
(1)对ZSNestedScrollView 滚动进行监听,滚动到底部时也就是TabLayout卡在顶部的时候,进行惯性处理
(2)如果速度不为0(有惯性需要处理)并且ZSNestedScrollView没有将惯性处理完,则子类自己处理
(3)子类找到RecyclerView,调用其fling方法
上面涉及到一个工具类FlingHelper 代码如下:
package com.zs.test.nestedscroll
import android.content.Context
import com.zs.test.nestedscroll.FlingHelper
import android.view.ViewConfiguration
class FlingHelper(context: Context)
private fun getSplineDeceleration(i: Int): Double
return Math.log(
(0.35f * Math.abs(i)
.toFloat() / (mFlingFriction * mPhysicalCoeff)).toDouble()
)
private fun getSplineDecelerationByDistance(d: Double): Double
return (DECELERATION_RATE.toDouble() - 1.0) * Math.log(d / (mFlingFriction * mPhysicalCoeff).toDouble()) / DECELERATION_RATE.toDouble()
fun getSplineFlingDistance(i: Int): Double
return Math.exp(getSplineDeceleration(i) * (DECELERATION_RATE.toDouble() / (DECELERATION_RATE.toDouble() - 1.0))) * (mFlingFriction * mPhysicalCoeff).toDouble()
fun getVelocityByDistance(d: Double): Int
return Math.abs((Math.exp(getSplineDecelerationByDistance(d)) * mFlingFriction.toDouble() * mPhysicalCoeff.toDouble() / 0.3499999940395355).toInt())
companion object
private val DECELERATION_RATE = (Math.log(0.78) / Math.log(0.9)).toFloat()
private val mFlingFriction = ViewConfiguration.getScrollFriction()
private var mPhysicalCoeff: Float = 0.0f
init
mPhysicalCoeff = context.resources.displayMetrics.density * 160.0f * 386.0878f * 0.84f
以上就是所有核心代码和逻辑。
以上是关于ViewPager自动滚动的主要内容,如果未能解决你的问题,请参考以下文章
ViewPager里面的ScrollView,自动滚动到中间
Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东美团首页效果Tab页滚动到顶部时自动吸附
Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东美团首页效果Tab页滚动到顶部时自动吸附
Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东美团首页效果Tab页滚动到顶部时自动吸附