Recyclerview实现跑马灯效果

405 阅读6分钟
最近写项目看到了一个跑马灯的需求,我说这还不简单,结果我看到真实效果后,我说还真有点不简单,我查了跑马灯并没有找到适合的,因为他是一个recyclerview进行跑马灯效果,本来我是想能用三方就用三方吧,手撸成本有点高,可我实在是没有找到只能手撸了直接看代码吧

6da14a69-4cbe-4e55-8ddf-c6d50eafbfe4.gif 既然是手撸,那就考虑的多一些不管是自动滚动,还是手动滚动,以及循环滚动,反转滚动 就都考虑到位,免得后面有变化的时候还的再回头写改

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)
    }
}