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

作者:一笑轮回
链接:https://juejin.cn/post/7380565157771247625
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Scroller 是一个用于实现平滑滚动效果的工具类。它可以用于在 Android 应用中实现滑动的动画效果,如平滑滚动到指定位置或者平滑滚动到顶部。 使用 Android Scroller 需要以下步骤: 1. 创建一个 Scroller 实例:使用 `new Scroller(context)` 创建一个 Scroller 对象。 2. 在 View 的 `computeScroll()` 方法中更新滚动位置:在需要实现滑动效果的 View 类里重写 `computeScroll()` 方法,然后在该方法中调用 `scroller.computeScrollOffset()` 获取当前的滚动位置,并根据需要更新 View 的位置。 3. 处理触摸事件:在触摸事件的回调方法中调用 Scroller 的 `startScroll()` 方法来启动滚动效果。可以根据触摸事件的不同情况调用不同的方法,如 `startScroll(int startX, int startY, int dx, int dy)` 或者 `startScroll(int startX, int startY, int dx, int dy, int duration)` 来指定滚动的起点、偏移量和持续时间。 4. 在 View 的 `invalidate()` 方法中不断重绘:在 `computeScroll()` 方法中更新了 View 的位置后,需要在 View 的 `invalidate()` 方法中调用,以便触发 View 的重新绘制。 需要注意的是,尽管 Android Scroller 提供了平滑滚动的功能,但它仅仅是一个工具类,实际的滚动效果实现还需要结合其他相关的 API 和组件来完成,如使用 `ViewGroup.LayoutParams` 来设置 View 的位置和大小,或者使用 `ViewPropertyAnimator` 实现更复杂的动画效果。 希望这个回答对你有帮助!如果有更多问题,请继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值