最近写项目看到了一个跑马灯的需求,我说这还不简单,结果我看到真实效果后,我说还真有点不简单,我查了跑马灯并没有找到适合的,因为他是一个recyclerview进行跑马灯效果,本来我是想能用三方就用三方吧,手撸成本有点高,可我实在是没有找到只能手撸了直接看代码吧
既然是手撸,那就考虑的多一些不管是自动滚动,还是手动滚动,以及循环滚动,反转滚动 就都考虑到位,免得后面有变化的时候还的再回头写改
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ViewGroup
import android.view.animation.Interpolator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.util.Calendar
import kotlin.math.abs
/**
* @author 浩楠
*
* @date 2024/8/17-15:53.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ | |/ _` | ___ | __| | | |/ _` | |/ _ \
* / ___ | | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ __| |_|__,_|_| ___/|_|__,_| |____/ __|__,_|__,_|_|___/
* @Description: TODO
*/
private const val SPEED = 40
private var xPushDown = 0f
private var yPushDown = 0f
private var startClickTime: Long = 0
class AutoScrollRecyclerview @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : RecyclerView(context, attrs, defStyle) {
/**
* 滑动插值器
*/
private var interpolator: UniformSpeedInterpolator = UniformSpeedInterpolator()
/**
* 水平方向的滑动距离
*/
private var speedDx = 0
/**
* 垂直方向的滑动距离
*/
private var speedDy: Int = 0
/**
* 滑动速度,默认值为40
*/
private var currentSpeed = SPEED
/**
* 是否启用无限滚动
*/
private var isLoopEnabled = false
/**
* 是否反向滑动
*/
private var isReverse = false
/**
* 是否开启自动滑动
*/
private var isOpenAuto = false
/**
* 用户是否可以手动滑动屏幕
*/
private var canTouch = false
/**
* 用户是否点击了屏幕
*/
private var pointTouch = false
/**
* 数据是否准备完毕
*/
private var isReady = false
/**
* 是否初始化完成
*/
private var inflate = false
/**
* 是否停止自动滚动
*/
private var isStopAutoScroll = false
/**
* 列表项点击事件监听器
*/
private var itemClickListener: ((ViewHolder?, Int) -> Unit)? = null
/**
* 开始自动滚动
*/
fun startAutoScroll() {
isStopAutoScroll = false
openAutoScroll(currentSpeed, false)
}
/**
* 开启自动滚动
*
* @param speed 滑动距离(决定滑动速度)
* @param reverse 是否反向滑动
*/
fun openAutoScroll(speed: Int = SPEED, reverse: Boolean = false) {
isReverse = reverse
currentSpeed = speed
isOpenAuto = true
notifyLayoutManager()
startScroll()
}
/**
* 设置是否可以手动滑动
*/
fun setCanTouch(b: Boolean) {
canTouch = b
}
/**
* 设置列表项点击事件监听器
*/
fun setItemClickListener(onItemClicked: (ViewHolder?, Int) -> Unit) {
itemClickListener = onItemClicked
}
/**
* 是否可以手动滑动
*/
fun canTouch(): Boolean {
return canTouch
}
/**
* 设置是否启用无限滚动
*/
fun setLoopEnabled(loopEnabled: Boolean) {
isLoopEnabled = loopEnabled
if (adapter != null) {
adapter!!.notifyDataSetChanged()
startScroll()
}
}
/**
* 是否启用无限滚动
*/
fun isLoopEnabled(): Boolean {
return isLoopEnabled
}
/**
* 设置是否反向滑动
*/
fun setReverse(reverse: Boolean) {
isReverse = reverse
notifyLayoutManager()
startScroll()
}
/**
* 暂停自动滚动
*
* @param isStopAutoScroll 是否停止自动滚动
*/
fun pauseAutoScroll(isStopAutoScroll: Boolean) {
this.isStopAutoScroll = isStopAutoScroll
}
/**
* 获取是否反向滑动
*/
fun getReverse(): Boolean {
return isReverse
}
/**
* 开始滚动
*/
private fun startScroll() {
if (!isOpenAuto) return
if (scrollState == SCROLL_STATE_SETTLING) return
if (inflate && isReady) {
speedDy = 0
speedDx = currentSpeed
smoothScroll()
}
}
/**
* 平滑滚动
*/
private fun smoothScroll() {
if (!isStopAutoScroll) {
val absSpeed = abs(currentSpeed)
val d = if (isReverse) -absSpeed else absSpeed
smoothScrollBy(d, d, interpolator)
}
}
/**
* 通知布局管理器
*/
private fun notifyLayoutManager() {
val layoutManager = layoutManager
if (layoutManager is LinearLayoutManager) {
val linearLayoutManager = layoutManager as LinearLayoutManager?
linearLayoutManager?.let { lm ->
lm.reverseLayout = isReverse
}
}
}
/**
* 替换适配器
*
* @param adapter 适配器
* @param removeAndRecycleExistingViews 是否移除并回收现有视图
*/
override fun swapAdapter(adapter: Adapter<*>?, removeAndRecycleExistingViews: Boolean) {
super.swapAdapter(generateAdapter(adapter!!), removeAndRecycleExistingViews)
isReady = true
}
/**
* 设置适配器
*
* @param adapter 适配器
*/
override fun setAdapter(adapter: Adapter<*>?) {
super.setAdapter(generateAdapter(adapter!!))
isReady = true
}
/**
* 拦截触摸事件
*
* @param e 触摸事件
* @return 是否拦截
*/
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
return if (canTouch) {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
pointTouch = true
xPushDown = e.x
yPushDown = e.y
startClickTime = Calendar.getInstance().timeInMillis
itemRvClicked()
continueScroll()
}
MotionEvent.ACTION_UP -> {
continueScroll()
return false
}
MotionEvent.ACTION_MOVE -> {
return true
}
}
super.onInterceptTouchEvent(e)
return false
} else false
}
/**
* 继续滚动
*/
private fun continueScroll() {
if (isOpenAuto) {
pointTouch = false
currentSpeed += 1
startScroll()
currentSpeed -= 1
}
}
/**
* 处理触摸事件
*
* @param e 触摸事件
* @return 是否处理
*/
override fun onTouchEvent(e: MotionEvent): Boolean {
return if (canTouch) {
when (e.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
continueScroll()
return false
}
}
super.onTouchEvent(e)
} else false
}
/**
* 执行点击事件
*
* @return 是否点击
*/
override fun performClick(): Boolean {
super.performClick()
return true
}
/**
* 处理RecyclerView项点击事件
*/
private fun itemRvClicked() {
val clickDuration: Long = Calendar.getInstance().timeInMillis - startClickTime
if (clickDuration < 200) {
val viewHolder: ViewHolder?
val actualPositionItem: Int
val child = this.findChildViewUnder(xPushDown, yPushDown)
if (child != null && itemClickListener != null) {
viewHolder = this.findContainingViewHolder(child)
viewHolder?.let {
actualPositionItem =
(this.adapter as NestingRecyclerViewAdapter).getActualPosition(viewHolder.adapterPosition)
itemClickListener?.invoke(viewHolder, actualPositionItem)
}
}
}
}
/**
* 当视图大小发生变化时调用
*/
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
startScroll()
}
/**
* 当视图完成膨胀时调用
*/
override fun onFinishInflate() {
super.onFinishInflate()
inflate = true
}
/**
* 当视图滚动时调用
*
* @param dx 水平方向滚动的距离
* @param dy 垂直方向滚动的距离
*/
override fun onScrolled(dx: Int, dy: Int) {
if (pointTouch) {
speedDx = 0
speedDy = 0
return
}
val vertical: Boolean
if (dx == 0) { // 垂直滚动
speedDy += dy
vertical = true
} else { // 水平滚动
speedDx += dx
vertical = false
}
if (vertical) {
if (abs(speedDy) >= abs(currentSpeed)) {
speedDy = 0
smoothScroll()
}
} else {
if (abs(speedDx) >= abs(currentSpeed)) {
speedDx = 0
smoothScroll()
}
}
}
/**
* 生成适配器
*
* @param adapter 适配器
* @return 嵌套的RecyclerView适配器
*/
private fun generateAdapter(adapter: Adapter<*>): NestingRecyclerViewAdapter<out ViewHolder> {
return NestingRecyclerViewAdapter(this, adapter)
}
/**
* 嵌套的RecyclerView适配器
*/
class NestingRecyclerViewAdapter<VH : ViewHolder>(
private val autoScrollRecyclerView: AutoScrollRecyclerview,
var adapter: Adapter<VH>
) : Adapter<VH>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
return adapter.onCreateViewHolder(parent, viewType)
}
override fun registerAdapterDataObserver(observer: AdapterDataObserver) {
super.registerAdapterDataObserver(observer)
adapter.registerAdapterDataObserver(observer)
}
override fun unregisterAdapterDataObserver(observer: AdapterDataObserver) {
super.unregisterAdapterDataObserver(observer)
adapter.unregisterAdapterDataObserver(observer)
}
override fun onBindViewHolder(holder: VH, position: Int) {
adapter.onBindViewHolder(holder, generatePosition(position))
}
override fun setHasStableIds(hasStableIds: Boolean) {
super.setHasStableIds(hasStableIds)
adapter.setHasStableIds(hasStableIds)
}
override fun getItemCount(): Int {
// 如果是无限滚动模式,设置无限数量的项
return if (getLoopEnable()) Int.MAX_VALUE else adapter.itemCount
}
override fun getItemViewType(position: Int): Int {
return adapter.getItemViewType(generatePosition(position))
}
override fun getItemId(position: Int): Long {
return adapter.getItemId(generatePosition(position))
}
/**
* 根据当前滚动模式返回对应位置
*/
fun generatePosition(position: Int): Int {
return if (getLoopEnable()) {
getActualPosition(position)
} else {
position
}
}
/**
* 返回项的实际位置
*
* @param position 滚动开始后的位置会无限增长
* @return 项的实际位置
*/
fun getActualPosition(position: Int): Int {
val itemCount = adapter.itemCount
return if (position >= itemCount) position % itemCount else position
}
/**
* 获取是否启用无限滚动
*/
private fun getLoopEnable(): Boolean {
return autoScrollRecyclerView.isLoopEnabled
}
/**
* 获取是否反向滚动
*/
fun getReverse(): Boolean {
return autoScrollRecyclerView.getReverse()
}
}
/**
* 自定义插值器
* 以恒定速度滑动列表
*/
private class UniformSpeedInterpolator : Interpolator {
override fun getInterpolation(input: Float): Float {
return input
}
}
}
既然已经手撸的列表了,那肯定要有一个配套的适配器啊
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
/**
* @author 浩楠
*
* @date 2024/8/17-15:53.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ | |/ _` | ___ | __| | | |/ _` | |/ _ \
* / ___ | | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ __| |_|__,_|_| ___/|_|__,_| |____/ __|__,_|__,_|_|___/
*
* @param T 数据模型类型
* @param VB ViewBinding类型
* @param myItems 数据列表
* @param inflate 视图绑定的inflate函数
* @param bindViewHolder 绑定视图的函数
*
* @Description: TODO 通用跑马灯RecyclerView适配器
*/
class AutoAdapter<T, VB : ViewBinding>(
private val myItems: MutableList<T>,
private val inflate: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val bindViewHolder: (VB, T, Int) -> Unit
) :
RecyclerView.Adapter<AutoAdapter<T, VB>.ViewHolder>() {
inner class ViewHolder(val binding: VB) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun getItemCount(): Int {
return myItems.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
bindViewHolder(holder.binding, myItems[position], position)
}
}
这下整整齐齐的一家人都有了,那用法就已经很简单明了了
activity布局文件
<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=".ui.activity.DemoActivity">
<com.ghn.demo.view.AutoScrollRecyclerview
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/DemoAutoRv"
tools:listitem="@layout/item_auto_rv"
/>
</LinearLayout>
recyclerview的布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
>
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@mipmap/ic_launcher"
/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/TvItemDemo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1111"
android:layout_gravity="center"
android:layout_marginStart="12dp"
/>
</LinearLayout>
实体类
/**
* @author 浩楠
*
* @date 2024/8/17-17:19.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ | |/ _` | ___ | __| | | |/ _` | |/ _ \
* / ___ | | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ __| |_|__,_|_| ___/|_|__,_| |____/ __|__,_|__,_|_|___/
* @Description: TODO
*/
data class LampBean (
val title:String
)
activity 中的使用
class DemoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_demo)
val mAutoRv = findViewById<AutoScrollRecyclerview>(R.id.DemoAutoRv)
mAutoRv.layoutManager= LinearLayoutManager(this)
val itemlamp = mutableListOf<LampBean>()
itemlamp.add(LampBean("滑动一"))
itemlamp.add(LampBean("滑动二"))
itemlamp.add(LampBean("滑动三"))
// 绑定视图并设置数据的函数
val lampbindview: (ItemAutoRvBinding, LampBean, Int) -> Unit = { binding, item, _ ->
binding.TvItemDemo.text = item.title
}
mAutoRv.adapter =
AutoAdapter(
myItems = itemlamp,
inflate = ItemAutoRvBinding::inflate,
bindViewHolder = lampbindview
)
//自动滚动
mAutoRv.startAutoScroll()
//支持手动和自动
mAutoRv.setCanTouch(true)
//循环滚动
mAutoRv.setLoopEnabled(true)
}
}