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

需求

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

效果

实现后的效果如下:

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

实现思路

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

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

实现代码

ScrollerLayout.kt

kotlin 复制代码
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

kotlin 复制代码
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

相关推荐
夜色呦1 小时前
深入 Laravel 的错误处理与异常处理机制
android·php·laravel
JoyceMill3 小时前
Android动画:提升用户体验的关键技术
android·ux
s_nshine3 小时前
将 build.gradle 配置从 Groovy 迁移到 Kotlin
android·开发语言·kotlin·gradle·groovy·build
Jerry_正弦3 小时前
Kotlin中object关键字的作用
android·开发语言·kotlin
buyue__3 小时前
Kotlin/Android中执行HTTP请求
android·开发语言·kotlin
JoyceMill3 小时前
Android 图像切换器:实现动态图像切换的关键技术与应用
android
RelinRan3 小时前
Android 遥控器
android
H1005 小时前
Kotlin中对空的很多处理
android·开发语言·kotlin
松仔log5 小时前
Kotlin基础——异步和并发
android·开发语言·kotlin
H1006 小时前
Kotlin中的关键字
android·kotlin