自定义View:循环垂直滚动的NestScrollView
前沿
上一篇用Scrollview实现了垂直循环滚动的View,
参考链接:
自定义View:循环垂直滚动的Recyclerview
本篇博客我们使用NestScrollView来实现相同的效果,效果如下图底部滚动的用户评论部分
实现思路分析
- 自定义View继承自NestScrollView
- 循环滚动通过handler不断发送延迟消息实现
- 通过NestedScrollView的scrollTo滚动到底部
- 通过NestedScrollView的smoothScrollBy每次滚动一个Item高度,来实现平滑的滚动
- 接受page_size参数,作为显示的条数
滚动的方法确定了,那么最关键的如何实现循环的效果呢?这里通过滚动到顶部后重制数据,并且直接滚动到底部,再继续滚动的方式,具体实现如下:
- 设置AutoScrollView总共包含的item个数为:取数据集大小和2倍page_size中的最小值,即: itemCount=min(Data.size, PageSize * 2)
- 初始化即创建itemCount个数的item布局
- 并计算一个Item的高度,总高度/pageSize,所以此布局需要设置一个固定的总高度
- 初始化时候滚动到底部,因为我们是需要从上往下不断的循环滚动
- 通过Handler 发送延迟消息,开启滚动
- Handler处理滚动消息,每次滚动一个Item的高度
- 最关键的处理来了:如果滚动到顶部了,应该怎么处理即scrollY=0的时候,这时候首先将当前顶部的数据(0至pageSize-1位置)设置到底部(pageSize至2*pageSize-1)的位置,然后重新设置pageSize之上的所有数据(0至pageSize-1),每一次取数据通过一个变量mLastDataPosition来获取,每次从数据集取数据后mLastDataPosition+1,最后通过scrollTo直接再此滚动到底部,这样一次循环就完成了,发送延迟消息继续滚动。
实现源码如下,可以直接拷贝运行查看效果
package com.hdp.testvie
import android.content.Context
import android.os.Handler
import android.os.Message
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.LayoutRes
import androidx.core.widget.NestedScrollView
import java.lang.ref.WeakReference
import kotlin.math.min
import kotlin.properties.Delegates
class AutoScrollView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : NestedScrollView(context, attrs, defStyleAttr) {
companion object {
val TAG = "ScrollView"
private val MESSAGE_SCROLL = 10010
}
private var mPageSize by Delegates.notNull<Int>() //显示几个
private var mItemCount by Delegates.notNull<Int>() //scrollView包含的元素总数
var mContentParentLayout: LinearLayout
private var mData = mutableListOf<Any>()
private var mLastDataPosition = 0 //最后显示数据的位置
private lateinit var mItemBuilder: ItemBuilder
private var mItemLayoutId by Delegates.notNull<Int>() //item布局id
private var itemHeight: Int = 100
private var maxScrollHeight: Int = 600
private var mLoopHandler: LoopHandler? = null
class LoopHandler(autoScrollView: AutoScrollView) : Handler() {
var weakreferedces: WeakReference<AutoScrollView>? = null
init {
weakreferedces = WeakReference(autoScrollView)
}
override fun handleMessage(msg: Message) {
val autoScrollView = weakreferedces?.get()
if (autoScrollView != null) {
when (msg.what) {
MESSAGE_SCROLL -> {
val scrollY = autoScrollView.scrollY
if (scrollY == 0) {
//到顶部了
autoScrollView.resetOnScrollToTop()
//移动到底部
autoScrollView.scrollTo(0, autoScrollView.maxScrollHeight)
//重新添加数据
autoScrollView.resetOnScrollToBottom()
postDelayed({
sendEmptyMessage(MESSAGE_SCROLL)
}, 60)
} else {
autoScrollView.smoothScrollBy(0, -autoScrollView.itemHeight)
postDelayed({
sendEmptyMessage(MESSAGE_SCROLL)
}, 3000)
}
}
}
}
}
}
override fun onTouchEvent(ev: MotionEvent?) = true
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AutoScrollView)
mPageSize = typedArray.getInt(R.styleable.AutoScrollView_page_size, 2)
mContentParentLayout = LinearLayout(context)
mContentParentLayout.orientation = LinearLayout.VERTICAL;
mContentParentLayout.layoutParams = ViewGroup.LayoutParams(-1, -1)
typedArray.recycle()
addView(mContentParentLayout)
mLoopHandler = LoopHandler(this)
}
fun setData(data: List<Any>, itemBuilder: ItemBuilder, @LayoutRes itemLayoutId: Int) {
mData.clear()
mData.addAll(data)
mItemBuilder = itemBuilder
mItemLayoutId = itemLayoutId
val containerHeight = measuredHeight
itemHeight = containerHeight / mPageSize
mItemCount = min(mData.size, mPageSize * 2)
maxScrollHeight = mItemCount * itemHeight
//添加Item
initItems()
//滚动到底部
mLoopHandler?.postDelayed({
scrollTo(0, maxScrollHeight)
}, 50)
//开始循环
if (mItemCount == mPageSize * 2) {
mLoopHandler?.postDelayed(
{
mLoopHandler?.sendEmptyMessage(MESSAGE_SCROLL)
},
3000
)
}
}
private fun initItems() {
val layoutParams = LinearLayout.LayoutParams(-1, itemHeight)
for (i in 0 until mItemCount) {
mLastDataPosition = i
Log.e(TAG, "init showPosition=$mLastDataPosition")
val itemLayout = View.inflate(context, mItemLayoutId, null)
mContentParentLayout.addView(itemLayout, 0, layoutParams)
mItemBuilder.buildItem(itemLayout, mData[i])
//设置tag
itemLayout.tag = mData[i]
}
}
/**
* 滚动到顶部时候,将顶部pageSize条数据设置到底部
*/
@Suppress("UNCHECKED_CAST")
private fun resetOnScrollToTop() {
//将顶部数据设置到底部
for (i in 0 until mPageSize) {
val topView = mContentParentLayout.getChildAt(i)
val topData = topView.tag
val lastView = mContentParentLayout.getChildAt(mPageSize + i)
mItemBuilder.buildItem(lastView, topData)
lastView.tag = topData
}
}
/**
* 快速滚动到底部后,重置pageSize之上的所有数据
*/
private fun resetOnScrollToBottom() {
for (i in mItemCount - mPageSize - 1 downTo 0) {
val itemView = mContentParentLayout.getChildAt(i)
mLastDataPosition = (mLastDataPosition + 1) % mData.size
mItemBuilder.buildItem(itemView, mData[mLastDataPosition])
itemView.tag = mData[mLastDataPosition]
}
}
/**
* 构建每个ite项
*/
interface ItemBuilder {
fun buildItem(itemView: View, data: Any)
}
override fun fling(velocityY: Int) {
super.fling(velocityY / 1000)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mLoopHandler?.removeCallbacksAndMessages(null)
}
}
善后处理
- 屏蔽NestedScrollView的触摸滚动,直接消费Touch事件即可
override fun onTouchEvent(ev: MotionEvent?) = true
- 生命周期管理
当视图从窗口移除的时候,我们需要停止Handler发送消息
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mLoopHandler?.removeCallbacksAndMessages(null)
}
- 通过Item构造器接口,方便外部直接构建Item视图
在布局文件使用
<com.hdp.testvie.AutoScrollView
android:id="@+id/auto_scroll"
android:layout_width="300dp"
app:page_size="3"
android:layout_height="300dp"
android:background="@color/colorAccent" />
item文件flip_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@android:color/white"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_item"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="20dp"
android:text="我是广告"
android:textSize="22sp" />
</LinearLayout>
</LinearLayout>
自定义Style 文件
<declare-styleable name="AutoScrollView">
<attr name="page_size" format="integer" />
</declare-styleable>
在Activity中设置数据
auto_scroll.post {
var scrollData = mutableListOf<String>()
for (i in 1..12) {
scrollData.add("$i")
}
auto_scroll.setData(
data = scrollData,
itemBuilder = object : AutoScrollView.ItemBuilder {
override fun buildItem(itemView: View, data: Any) {
val tv = itemView.findViewById<TextView>(R.id.tv_item)
val iv = itemView.findViewById<ImageView>(R.id.iv_item)
tv.text = "我是广告哈哈哈:$data"
}
},
itemLayoutId = R.layout.flip_item
)
}
用到的文件都在上面了,最后,看一下效果图,gif录制效果不是很理想
注意点:
AutoScrollView需要设置一个固定的总高度,总高度=一个Item的高度*pageSize,笔者这里一个Item的高度是100,需要显示3个,即pageSize设置成3,故总高度设置成300,
Thinks!