• Android采用Scroller实现底部二楼效果


    需求

    移动应用开发中,有时我们希望实现一种特殊的布局效果,即“底部二楼”效果。这个效果类似于在列表底部拖动时出现额外的内容区域,用户可以继续向上拖动查看更多内容。这种效果可以用于展示广告、推荐内容或其他信息。

    效果

    实现后的效果如下:

    1. 当用户滑动到列表底部时,可以继续向上拖动,显示出隐藏的底部内容区域。
    2. 底部内容区域可以包含任意视图,如RecyclerView等。
    3. 滑动到一定阈值后,可以自动回弹到初始位置或完全展示底部内容。

    实现思路

    为了实现这一效果,我们可以自定义一个ScrollerLayout,并使用Scroller类来处理滑动和回弹动画。主要思路如下:

    1. 创建自定义的ScrollerLayout继承自LinearLayout
    2. ScrollerLayout中,遍历所有子视图,找到其中的RecyclerView,并为其添加滚动监听器。
    3. RecyclerView滚动到顶部时,允许整个布局继续向上滑动,展示底部内容区域。
    4. 使用Scroller类实现平滑滚动和回弹效果。

    实现代码

    ScrollerLayout.kt

    package com.yxlh.androidxy.demo.ui.scroller
    
    import android.content.Context
    import android.util.AttributeSet
    import android.util.Log
    import android.view.MotionEvent
    import android.view.View
    import android.view.ViewConfiguration
    import android.widget.LinearLayout
    import android.widget.Scroller
    import androidx.recyclerview.widget.RecyclerView
    import com.yxlh.androidxy.R
    
    //github.com/yixiaolunhui/AndroidXY
    class ScrollerLayout @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0,
    ) : LinearLayout(context, attrs, defStyleAttr) {
    
        private val mScroller = Scroller(context)
        private var lastY = 0
        private var downY = 0
        private var contentHeight = 0
        private var isRecyclerViewAtTop = false
        private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    
        init {
            orientation = VERTICAL
            post {
                setupRecyclerViews()
            }
        }
    
        private fun setupRecyclerViews() {
            for (i in 0 until childCount) {
                val child = getChildAt(i)
                if (child is RecyclerView) {
                    child.addOnScrollListener(object : RecyclerView.OnScrollListener() {
                        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                            isRecyclerViewAtTop = !recyclerView.canScrollVertically(-1)
                        }
                    })
                }
            }
        }
    
        override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
            super.onLayout(changed, l, t, r, b)
            val bottomBar = getChildAt(0)
            contentHeight = 0
            for (i in 0 until childCount) {
                val child = getChildAt(i)
                if (child is RecyclerView) {
                    contentHeight += child.measuredHeight
                }
            }
            bottomBar.layout(0, measuredHeight - bottomBar.measuredHeight, measuredWidth, measuredHeight)
            for (i in 1 until childCount) {
                val child = getChildAt(i)
                if (child is RecyclerView) {
                    child.layout(0, measuredHeight, measuredWidth, measuredHeight + contentHeight)
                }
            }
        }
    
        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
            val isTouchChildren = isTouchInsideChild(ev)
            Log.d("121212", "onInterceptTouchEvent isTouchChildren=$isTouchChildren")
            when (ev.action) {
                MotionEvent.ACTION_DOWN -> {
                    downY = ev.y.toInt()
                    lastY = downY
                }
    
                MotionEvent.ACTION_MOVE -> {
                    val currentY = ev.y.toInt()
                    val dy = currentY - downY
    
                    if (isRecyclerViewAtTop && dy > touchSlop) {
                        lastY = currentY
                        return true
                    }
                }
            }
            return super.onInterceptTouchEvent(ev)
        }
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    if (!isTouchInsideChild(event)) return false
                    if (!mScroller.isFinished) {
                        mScroller.abortAnimation()
                    }
                    lastY = event.y.toInt()
                    return true
                }
    
                MotionEvent.ACTION_MOVE -> {
                    if (!isTouchInsideChild(event)) return false
                    val currentY = event.y.toInt()
                    val dy = lastY - currentY
                    val scrollY = scrollY + dy
    
                    if (scrollY < 0) {
                        scrollTo(0, 0)
                    } else if (scrollY > contentHeight) {
                        scrollTo(0, contentHeight)
                    } else {
                        scrollBy(0, dy)
                    }
    
                    lastY = currentY
                    return true
                }
    
                MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                    val threshold = contentHeight / 2
                    if (scrollY > threshold) {
                        showNavigation()
                    } else {
                        closeNavigation()
                    }
                    return true
                }
            }
            return false
        }
    
        private fun isTouchInsideChild(event: MotionEvent): Boolean {
            val x = event.rawX.toInt()
            val y = event.rawY.toInt()
            for (i in 0 until childCount) {
                val child = getChildAt(i)
                if (isViewUnder(child, x, y)) {
                    return true
                }
            }
            return false
        }
    
        private fun isViewUnder(view: View?, x: Int, y: Int): Boolean {
            if (view == null) return false
            val location = IntArray(2)
            view.getLocationOnScreen(location)
            val viewX = location[0]
            val viewY = location[1]
            return x >= viewX && x < viewX + view.width && y >= viewY && y < viewY + view.height
        }
    
        fun showNavigation() {
            val dy = contentHeight - scrollY
            mScroller.startScroll(scrollX, scrollY, 0, dy, 500)
            invalidate()
        }
    
        private fun closeNavigation() {
            val dy = -scrollY
            mScroller.startScroll(scrollX, scrollY, 0, dy, 500)
            invalidate()
        }
    
        override fun computeScroll() {
            if (mScroller.computeScrollOffset()) {
                scrollTo(mScroller.currX, mScroller.currY)
                postInvalidateOnAnimation()
            }
        }
    }
    

    ScrollerActivity.kt

    package com.yxlh.androidxy.demo.ui.scroller
    
    import android.graphics.Color
    import android.os.Bundle
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.TextView
    import androidx.appcompat.app.AppCompatActivity
    import androidx.recyclerview.widget.LinearLayoutManager
    import androidx.recyclerview.widget.RecyclerView
    import com.yxlh.androidxy.R
    import com.yxlh.androidxy.databinding.ActivityScrollerBinding
    import kotlin.random.Random
    
    class ScrollerActivity : AppCompatActivity() {
    
        private var binding: ActivityScrollerBinding? = null
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityScrollerBinding.inflate(layoutInflater)
            setContentView(binding?.root)
    
            //内容布局
            binding?.content?.layoutManager = LinearLayoutManager(this)
            binding?.content?.adapter = ColorAdapter(false)
    
            //底部布局
            binding?.bottomContent?.layoutManager = LinearLayoutManager(this)
            binding?.bottomContent?.adapter = ColorAdapter(true)
    
            binding?.content?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
                override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                    if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
                        binding?.scrollerLayout?.showNavigation()
                    }
                }
            })
        }
    }
    
    class ColorAdapter(private var isColor: Boolean) : RecyclerView.Adapter<ColorAdapter.ColorViewHolder>() {
    
        private val colors = List(100) { getRandomColor() }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ColorViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.item_color, parent, false)
            return ColorViewHolder(view, isColor)
        }
    
        override fun onBindViewHolder(holder: ColorViewHolder, position: Int) {
            holder.bind(colors[position], position)
        }
    
        override fun getItemCount(): Int = colors.size
    
        private fun getRandomColor(): Int {
            val random = Random.Default
            return Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256))
        }
    
        class ColorViewHolder(itemView: View, private var isColor: Boolean) : RecyclerView.ViewHolder(itemView) {
            fun bind(color: Int, position: Int) {
                if (isColor) {
                    itemView.setBackgroundColor(color)
                }
                itemView.findViewById<TextView>(R.id.color_tv).text = "$position"
            }
    
    
        }
    }
    

    结束

    通过上述代码,我们成功实现了底部二楼效果。在用户滑动到RecyclerView底部时,可以继续向上拖动以显示底部的内容区域。这种效果可以增强用户体验,增加更多的内容展示方式。通过自定义布局和使用Scroller类,我们可以轻松实现这种复杂的滑动效果。
    详情:github.com/yixiaolunhui/AndroidXY

  • 相关阅读:
    最优传输(Optimal Transport)
    【Vue】TypeError: Cannot read properties of undefined (reading ‘xxx‘)
    因特网采用的标准网络协议是什么
    nginx 集群部署
    Redis数据类型——set类型数据介绍及操作
    META在2022年提出的最新创新
    9-6 Prometheus告警通知Alertmanager,结合邮箱,钉钉,企业微信实现告警,告警模板使用,告警分类发送
    基于STM32单片机的智能家居环境监测与控制系统设计
    docker学习入门篇
    spring多数据源动态切换的实现原理及读写分离的应用
  • 原文地址:https://blog.csdn.net/u014741977/article/details/139703479