Android | RecyclerView + OverScroller + TouchEvent实现左滑查看更多


本文将通过示例代码展示如何使用 RecyclerViewOverScrollerTouchEvent 实现一个具有左滑查看更多功能的横向滑动视图。

效果图

左滑查看更多

实现思路

主要通过以下几个步骤来实现:

  1. 创建自定义 HorizontalPicScrollerView 继承自 LinearLayout
  2. 使用 RecyclerView 实现横向滑动的图片列表。
  3. 利用 OverScroller 实现平滑滚动效果。
  4. 通过 TouchEvent 实现滑动拦截和事件处理。

核心代码解析

1. 自定义 HorizontalPicScrollerView

首先,创建一个自定义的 HorizontalPicScrollerView 继承自 LinearLayout,并初始化相关属性和视图:

class HorizontalPicScrollerView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0,
) : LinearLayout(context, attrs, defStyle) {

    companion object {
        const val TYPE_DATA = 1 // 正常样式
        const val TYPE_LOAD_MORE = 2 // 加载更多样式
        const val PIC_LEAST_NUM = 6
    }

    private val mRvPic: RecyclerView by id(R.id.rv_pic)
    private val mLoadMoreContainer: LinearLayout by id(R.id.load_more_container)
    private var mNeedIntercept: Boolean = false // 父View是否拦截事件

    private var mLastX = 0f
    private var mLastDownY = 0f
    private var mLastDownX = 0f // 用于判断滑动方向

    private var mMenuWidth = 0 // 加载更多View的宽度
    private var mShowMoreMenuWidth = 0 // 加载更多发生变化时的宽度

    private var mLoadMoreAction: (() -> Unit)? = null
    private var mScroller: OverScroller
    private var isTouchLeft = false // 是否是向左滑动

    init {
        orientation = HORIZONTAL
        View.inflate(context, R.layout.scroller_horizontal_pic, this)
        mScroller = OverScroller(context)
    }

2. 设置数据和布局管理器

setData 方法中,我们为 RecyclerView 设置数据和布局管理器:

    fun setData(
        models: MutableList<ItemPicInfo>,
        itemClick: (ItemPicInfo) -> Unit,
        loadMoreAction: () -> Unit,
    ) {
        this.mLoadMoreAction = loadMoreAction
        val layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
        mRvPic.layoutManager = layoutManager
        val picAdapter = PicAdapter(context, loadMoreAction).apply {
            addPicList(models)
            setOnItemClickListener { item -> itemClick.invoke(item) }
        }
        mRvPic.adapter = picAdapter
        mRvPic.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                //滑动到末尾时,如果继续左滑,事件将由父view接管
                val lastVisiblePos = layoutManager.findLastCompletelyVisibleItemPosition()
                val visibleCount = layoutManager.childCount
                val totalCount = layoutManager.itemCount
                mNeedIntercept =
                    totalCount >= PIC_LEAST_NUM && visibleCount > 0 && lastVisiblePos == totalCount - 1
            }
        })
    }

3. 事件分发和拦截

通过重写 dispatchTouchEventonInterceptTouchEventonTouchEvent 方法,实现滑动事件的分发和拦截:

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                mLastX = ev.x
                mLastDownY = ev.y
                mLastDownX = ev.x
                parent.requestDisallowInterceptTouchEvent(true)
            }

            MotionEvent.ACTION_MOVE -> {
                isTouchLeft = mLastDownX - ev.x > 0 // 判断滑动方向
                val dx = (ev.x - mLastDownX).absoluteValue
                val dy = (ev.y - mLastDownY).absoluteValue
                if (dy > dx) parent.requestDisallowInterceptTouchEvent(false)
            }
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var isIntercept = false
        when (ev?.action) {
            MotionEvent.ACTION_MOVE -> {
                val mDeltaX = mLastX - ev.x
                // 向左滑动拦截,否则不拦截
                isIntercept = if (mDeltaX > 0) mNeedIntercept else false
            }
        }
        return isIntercept
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_MOVE -> {
                val mDeltaX = mLastX - ev.x
                if (mDeltaX > 0) {
                    // 向左滑动
                    if (mDeltaX >= mMenuWidth || scrollX + mDeltaX >= mMenuWidth) {
                        // 右边缘检测
                        scrollTo(mMenuWidth, 0)
                        return true
                    }
                } else if (mDeltaX < 0) {
                    // 向右滑动
                    if (scrollX + mDeltaX <= 0) {
                        // 左边缘检测
                        scrollTo(0, 0)
                        return true
                    }
                }
                scrollBy(mDeltaX.toInt(), 0)
                mLastX = ev.x
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                smoothCloseMenu()
                mNeedIntercept = false
                // 执行回调
                val mDeltaX = mLastX - ev.x
                if (scrollX + mDeltaX >= mShowMoreMenuWidth) {
                    mLoadMoreAction?.invoke()
                }
            }
        }
        return super.onTouchEvent(ev)
    }

4. 平滑滚动

通过 OverScroller 实现平滑滚动效果:

    private fun smoothCloseMenu() {
        mScroller.forceFinished(true)
        /**
         * 左上为正,右下为负
         * startX:X轴开始位置
         * startY: Y轴结束位置
         * dx:X轴滑动距离
         * dy:Y轴滑动距离
         * duration:滑动时间
         */
        mScroller.startScroll(scrollX, 0, -scrollX, 0, 300)
        invalidate()
    }

    override fun computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            invalidate()
        }
    }

5. 图片适配器 PicAdapter

创建 PicAdapter 用于加载图片数据,并设置点击事件和加载更多功能:

class PicAdapter(private val mContext: Context, private val loadMoreAction: () -> Unit) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private var mPicList: MutableList<ItemPicInfo> = mutableListOf()
    private var mItemClick: ((ItemPicInfo) -> Unit)? = null

    fun setOnItemClickListener(onItemClick: (ItemPicInfo) -> Unit) {
        this.mItemClick = onItemClick
    }

    fun addPicList(bookList: List<ItemPicInfo>) {
        mPicList.clear()
        mPicList.addAll(bookList)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == TYPE_LOAD_MORE) {
            LoadMoreHolder(
                LayoutInflater.from(mContext)
                    .inflate(R.layout.item_pic_load_more, parent, false)
            )
        } else {
            PicHolder(
                LayoutInflater.from(mContext)
                    .inflate(R.layout.item_pic_show_view, parent, false)
            )
        }
    }

    override fun getItemViewType(position: Int): Int {
        if (mPicList.isEmpty() || position > mPicList.lastIndex) return TYPE_DATA
        return mPicList[position].dataType
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (mPicList.isEmpty() || position > mPicList.lastIndex) return
        val model: ItemPicInfo = mPicList[position]
        if (holder is PicHolder) {
            clipToRoundView(holder.itemView)
            processOnPicInfo(holder, model)
            holder.itemView.setOnClickListener { mItemClick?.invoke(model) }
        } else if (holder is LoadMoreHolder) {
            holder.itemView.setOnClickListener { loadMoreAction.invoke() }
        }
    }

    override fun getItemCount(): Int = mPicList.size

    class LoadMoreHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    class PicHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
       var ivPic: ImageView = itemView.findViewById(R.id.iv_book)
    }

    private fun processOnPicInfo(holder: PicHolder, model: ItemPicInfo) {
      //加载url
      Glide.with(holder.ivPic.context)
        .load(model.picUrl)
        .placeholder(R.drawable.icon_flower)
        .error(R.drawable.icon_flower)
        .into(holder.ivPic)
}

private fun clipToRoundView(view: View?) {
    view?.outlineProvider = object : ViewOutlineProvider() {
        override fun getOutline(view: View?, outline: Outline?) {
            if (view == null) return
            //设置矩形
            outline?.setRoundRect(0, 0, view.width, view.height, 12.dp2px().toFloat())
        }
    }
    view?.clipToOutline = true
  }
)

    class PicHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var ivPic: ImageView = itemView.findViewById(R.id.iv_book)
    }

    private fun processOnPicInfo(holder: PicHolder, model: ItemPicInfo) {
        //加载url
        Glide.with(holder.ivPic.context)
            .load(model.picUrl)
            .placeholder(R.drawable.icon_flower)
            .error(R.drawable.icon_flower)
            .into(holder.ivPic)
    }

    private fun clipToRoundView(view: View?) {
        view?.outlineProvider = object : ViewOutlineProvider() {
            override fun getOutline(view: View?, outline: Outline?) {
                if (view == null) return
                //设置矩形
                outline?.setRoundRect(0, 0, view.width, view.height, 12.dp2px().toFloat())
            }
        }
        view?.clipToOutline = true
    }
}

6. 数据类 ItemPicInfo

定义 ItemPicInfo 数据类,用于表示图片信息:

data class ItemPicInfo(
    val picUrl: String = "",
    val dataType: Int = TYPE_DATA,
)

总结

通过以上步骤,我们实现了一个基于 RecyclerViewOverScrollerTouchEvent 的自定义横向滑动视图,能够实现左滑查看更多功能。可以用于图片浏览,还可以扩展应用于其他类型的横向滑动视图,希望本文对你在开发过程中有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_小马快跑_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值