首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

定义列表的弹簧边缘效果

  • 24-12-16 12:05
  • 4065
  • 6227
juejin.cn

自定义弹簧边缘效果:RecyclerView 和 ListView 的解决方案

在开发 Android 应用时,为用户提供流畅且直观的交互体验是非常重要的。其中一种方法是通过自定义视图组件的滚动行为来增强用户体验。本文将介绍如何 为 RecyclerView 和 ListView 实现类似弹簧的边缘效果。

一、RecyclerView 的自定义弹簧边缘效果

1、使用 EdgeEffectFactory 创建自定义边缘效果

EdgeEffectFactory 是 Android 支持库(现在是 AndroidX)中的一部分,用于在 RecyclerView 边缘被拉动时创建边缘效果。默认的边缘效果是一个阻尼效果,当用户滚动到列表或网格的顶部或底部并且继续尝试滚动时会看到这个效果。为了实现更吸引人的弹簧效果,我们可以扩展 RecyclerView.EdgeEffectFactory 并重写 createEdgeEffect 方法。

2、SpringEdgeEffectFactory 类

以下是 SpringEdgeEffectFactory 类的具体实现:

kotlin
代码解读
复制代码
import android.view.View import androidx.core.view.ViewCompat import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.DIRECTION_BOTTOM import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.DIRECTION_TOP import android.graphics.Canvas import android.view.animation.DecelerateInterpolator import android.animation.ValueAnimator /** * 自定义 EdgeEffectFactory,用于为 RecyclerView 创建弹簧效果的边缘效果。 * 当用户拉伸 RecyclerView 的边缘时,子视图会根据拉伸距离进行平移, * 并在释放后返回原始位置,模拟弹簧效果。 */ class SpringEdgeEffectFactory : EdgeEffectFactory() { /** * 创建一个新的 EdgeEffect 实例。 * * @param view RecyclerView 实例。 * @param direction 拉动的方向,可以是 DIRECTION_TOP 或 DIRECTION_BOTTOM。 * @return 一个新的 EdgeEffect 实例。 */ @NonNull override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect { return object : EdgeEffect(view.context) { /** * 处理拉伸事件,根据拉伸距离调整子视图的位置。 */ private fun handlePull(deltaDistance: Float) { // 根据方向设置正负号 val sign = if (direction == DIRECTION_BOTTOM) -1 else 1 // 计算要移动的距离 val translationYDelta = sign * view.width * deltaDistance * 0.8f // 遍历所有可见的子视图并更新它们的位置 for (i in 0 until view.childCount) { val child = view.getChildAt(i) if (child.visibility == View.VISIBLE) { // 使用 ViewCompat 来确保兼容性 ViewCompat.setTranslationY(child, ViewCompat.getTranslationY(child) + translationYDelta) } } } override fun onPull(deltaDistance: Float) { super.onPull(deltaDistance) handlePull(deltaDistance) } override fun onPull(deltaDistance: Float, displacement: Float) { super.onPull(deltaDistance, displacement) handlePull(deltaDistance) } override fun onRelease() { super.onRelease() // 在释放后,创建动画让所有子视图回到原来的位置 for (i in 0 until view.childCount) { val child = view.getChildAt(i) ValueAnimator.ofFloat(ViewCompat.getTranslationY(child), 0f).apply { duration = 500 interpolator = DecelerateInterpolator(2.0f) addUpdateListener { animation -> ViewCompat.setTranslationY(child, animation.animatedValue as Float) } start() } } } override fun onAbsorb(velocity: Int) { super.onAbsorb(velocity) } override fun draw(canvas: Canvas): Boolean { setSize(0, 0) return super.draw(canvas) } } } }

使用说明:

  • 将 SpringEdgeEffectFactory 类加入到您的项目中。
  • 在您需要应用此效果的 RecyclerView 上调用 setEdgeEffectFactory() 方法,并传入 SpringEdgeEffectFactory()。

例如:

kotlin
代码解读
复制代码
val recyclerView: RecyclerView = findViewById(R.id.recycler_view) recyclerView.edgeEffectFactory = SpringEdgeEffectFactory()

这段代码将为指定的 RecyclerView 设置弹簧边缘效果。

二、ListView 的弹簧边缘效果

对于 ListView,由于它不像 RecyclerView 那样提供直接的 API 来定制边缘效果,因此我们需要手动处理滚动事件和动画来模拟类似的弹簧效果。

1、SpringListView 类

以下是 SpringListView 类的具体实现:

kotlin
代码解读
复制代码
import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.widget.EdgeEffect import android.widget.ListView /** * 自定义 ListView,实现了边缘回弹效果。 * 当用户拉伸 ListView 的边缘时,子视图会根据拉伸距离进行平移, * 并在释放后返回原始位置,模拟弹簧效果。 */ class SpringListView(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ListView(context, attrs, defStyleAttr) { /** * Companion object 用来存储静态变量和方法。 * MAX_OVERSCROLL_DISTANCE 设置了最大过滚动距离,防止过度拉伸。 */ private companion object { const val MAX_OVERSCROLL_DISTANCE = 200 // 设置最大过滚动距离 } /** * startY 用于记录触摸事件开始的 Y 轴坐标。 * isBeingDragged 标志位,表示当前是否正在拖动列表。 */ private var startY = 0f private var isBeingDragged = false /** * edgeEffectTop 和 edgeEffectBottom 分别是顶部和底部的 EdgeEffect 实例, * 用于创建当用户拉动列表边缘时的效果。 */ private val edgeEffectTop = EdgeEffect(context) private val edgeEffectBottom = EdgeEffect(context) /** * 重写 onTouchEvent 方法来处理触摸事件。 * ACTION_DOWN:记录触摸点的初始位置。 * ACTION_MOVE:判断是否已经开始拖动,并调用 handleScroll 处理滚动。 * ACTION_UP 和 ACTION_CANCEL:当手指离开屏幕时触发,结束拖动并释放滚动。 */ override fun onTouchEvent(ev: MotionEvent): Boolean { when (ev.action) { MotionEvent.ACTION_DOWN -> { // 记录触摸起始点的 Y 坐标 startY = ev.y // 标记为未拖拽状态 isBeingDragged = false } MotionEvent.ACTION_MOVE -> { // 如果尚未标记为拖拽状态,并且移动距离超过 touchSlop,则认为开始拖拽 if (!isBeingDragged && Math.abs(ev.y - startY) > touchSlop) { isBeingDragged = true } // 如果已经是拖拽状态,则调用 handleScroll 来处理滚动逻辑 if (isBeingDragged) { handleScroll(ev.y - startY) return true // 消费此事件 } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { // 手指离开屏幕时,如果之前处于拖拽状态,则调用 releaseScroll 结束动画 if (isBeingDragged) { releaseScroll() // 重置拖拽标志位 isBeingDragged = false } } } // 将事件传递给父类处理 return super.onTouchEvent(ev) } /** * handleScroll 函数用于处理垂直方向上的滚动。 * 它检查是否达到了列表的顶部或底部,并相应地调用 onPull 来启动边缘效果。 */ private fun handleScroll(distanceY: Float) { // 获取当前的滚动偏移量和滚动范围 val scrollY = computeVerticalScrollOffset() val scrollRange = computeVerticalScrollRange() - computeVerticalScrollExtent() // 如果向上滚动并且已经到达顶部,则触发顶部边缘效果 if (distanceY > 0 && scrollY <= 0) { onPull(distanceY, edgeEffectTop) } // 如果向下滚动并且已经到达底部,则触发底部边缘效果 else if (distanceY < 0 && scrollY >= scrollRange) { onPull(-distanceY, edgeEffectBottom) } } /** * onPull 函数负责更新 EdgeEffect 的状态。 * 它接收一个距离参数和一个 EdgeEffect 对象,然后调用其 onPull 方法。 */ private fun onPull(distance: Float, edgeEffect: EdgeEffect) { // 只有当边缘效果没有完成时才更新它的状态 if (!edgeEffect.isFinished) { // 更新边缘效果的状态,确保不超过 1 的比例 edgeEffect.onPull(Math.min(1f, distance / height)) // 请求重新绘制以显示新的边缘效果 invalidate() } } /** * releaseScroll 函数用于在手指离开屏幕后释放滚动。 * 它分别对顶部和底部的 EdgeEffect 调用 onRelease 方法,使得边缘效果可以自然地结束。 */ private fun releaseScroll() { // 释放顶部边缘效果 if (!edgeEffectTop.isFinished) { edgeEffectTop.onRelease() postInvalidateOnAnimation() // 触发下一个绘制周期 } // 释放底部边缘效果 if (!edgeEffectBottom.isFinished) { edgeEffectBottom.onRelease() postInvalidateOnAnimation() // 触发下一个绘制周期 } } /** * 重写 overScrollBy 方法,限制过滚动的最大距离。 */ override fun overScrollBy( deltaX: Int, deltaY: Int, scrollX: Int, scrollY: Int, scrollRangeX: Int, scrollRangeY: Int, maxOverScrollX: Int, maxOverScrollY: Int, isTouchEvent: Boolean ): Boolean { // 应用最大过滚动距离 val finalDeltaY = if (Math.abs(deltaY) > MAX_OVERSCROLL_DISTANCE) { if (deltaY > 0) MAX_OVERSCROLL_DISTANCE else -MAX_OVERSCROLL_DISTANCE } else deltaY // 调用父类的方法,传入调整后的 delta 值 return super.overScrollBy( deltaX, finalDeltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, MAX_OVERSCROLL_DISTANCE, isTouchEvent ) } /** * 重写 dispatchDraw 方法,在绘制所有子项之后绘制边缘效果。 */ override fun dispatchDraw(canvas: Canvas) { // 先绘制所有的子项 super.dispatchDraw(canvas) // 然后绘制顶部和底部的边缘效果 drawEdgeEffect(canvas, edgeEffectTop, 0, 0) drawEdgeEffect(canvas, edgeEffectBottom, height, height) } /** * drawEdgeEffect 函数负责在 Canvas 上绘制指定的 EdgeEffect。 * 它接受一个 Canvas 对象、一个 EdgeEffect 对象以及要应用的平移和尺寸信息。 */ private fun drawEdgeEffect(canvas: Canvas, edgeEffect: EdgeEffect, translateY: Int, sizeY: Int) { // 保存当前画布状态 val restoreCount = canvas.save() // 平移画布到指定位置 canvas.translate(0f, translateY.toFloat()) // 设置边缘效果的大小 edgeEffect.setSize(width, height) // 如果边缘效果需要绘制,则调用其 draw 方法并在下一帧请求重绘 if (edgeEffect.draw(canvas)) { postInvalidateOnAnimation() } // 恢复画布状态 canvas.restoreToCount(restoreCount) } }

注释总结:

  • Companion Object:包含了一个常量 MAX_OVERSCROLL_DISTANCE,它定义了用户可以超出列表边界的最大距离。
  • 成员变量:
    • startY 用于跟踪手指按下的起始位置。
    • isBeingDragged 是一个布尔值,用于标识用户是否正在拖拽列表。
    • edgeEffectTop 和 edgeEffectBottom 是两个 EdgeEffect 实例,分别对应顶部和底部的边缘效果。
  • onTouchEvent:重写了触摸事件处理函数,通过监听手指的动作来控制何时开始和结束拖拽行为。
  • handleScroll:计算滚动偏移量,并决定是否应该激活边缘效果。
  • onPull:更新特定 EdgeEffect 的状态,以反映用户的拖拽动作。
  • releaseScroll:当用户结束拖拽时,让边缘效果自然地结束。
  • overScrollBy:限制了过滚动的距离,避免过度拉伸。
  • dispatchDraw:在绘制完所有子视图之后,再绘制边缘效果。
  • drawEdgeEffect:具体实现如何将 EdgeEffect 绘制到 Canvas 上。

2、SpringListView 简单版实现

java
代码解读
复制代码
public class SpringListView extends ListView { // 创建两个EdgeEffect实例来分别处理顶部和底部的边缘回弹效果。 private EdgeEffect edgeEffectTop; private EdgeEffect edgeEffectBottom; // 用于跟踪当前是否正在发生过量滚动(overscroll)。 private boolean isOverscrolling; public SpringListView(Context context) { super(context); init(); } public SpringListView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public SpringListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { // 初始化顶部和底部的EdgeEffect。 edgeEffectTop = new EdgeEffect(getContext()); edgeEffectBottom = new EdgeEffect(getContext()); } @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { final boolean canScrollUp = getFirstVisiblePosition() == 0 && getChildAt(0).getTop() >= 0; final boolean canScrollDown = getLastVisiblePosition() == getCount() - 1 && getChildAt(getChildCount() - 1).getBottom() <= getHeight(); if (deltaY < 0 && canScrollUp) { // 处理顶部过量滚动 handleEdgeEffect(edgeEffectTop, deltaY / getHeight(), true); } else if (deltaY > 0 && canScrollDown) { // 处理底部过量滚动 handleEdgeEffect(edgeEffectBottom, deltaY / getHeight(), false); } else { // 如果没有过量滚动,则重置标志位。 isOverscrolling = false; } return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); } private void handleEdgeEffect(EdgeEffect edgeEffect, float deltaDistance, boolean isTop) { if (!edgeEffect.isFinished()) { edgeEffect.onPull(deltaDistance); invalidate(); isOverscrolling = true; } } @Override public void computeScroll() { super.computeScroll(); if (isOverscrolling) { if (!edgeEffectTop.isFinished()) { edgeEffectTop.onRelease(); postInvalidateOnAnimation(); } else if (!edgeEffectBottom.isFinished()) { edgeEffectBottom.onRelease(); postInvalidateOnAnimation(); } else { isOverscrolling = false; } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int height = getHeight(); final int width = getWidth(); // 绘制顶部EdgeEffect if (isOverscrolling && !edgeEffectTop.isFinished()) { edgeEffectTop.setSize(width, height); if (edgeEffectTop.draw(canvas)) { postInvalidateOnAnimation(); } } // 绘制底部EdgeEffect if (isOverscrolling && !edgeEffectBottom.isFinished()) { canvas.translate(0, height); edgeEffectBottom.setSize(width, height); if (edgeEffectBottom.draw(canvas)) { postInvalidateOnAnimation(); } canvas.translate(0, -height); } } }

这段代码实现了以下功能:

  • 它创建了两个EdgeEffect对象,一个用于顶部,一个用于底部。
  • 在overScrollBy方法中,它检查用户是尝试在顶部还是底部进行过量滚动,并相应地激活对应的EdgeEffect。
  • handleEdgeEffect辅助方法简化了对EdgeEffect的操作。
  • computeScroll方法确保当任何一个EdgeEffect动画未完成时,都会继续播放直到结束。
  • onDraw方法根据需要绘制顶部或底部的EdgeEffect,并保证动画能够正确显示。

这样,无论是在顶部下拉还是在底部上滑,SpringListView都会提供一个自然的回弹效果。

使用说明:

  • 将 SpringListView 类添加到项目中。
  • 在 XML 布局文件中使用 SpringListView 替换普通的 ListView。
  • 确保您的布局文件和 Activity/Fragment 中正确引用了 SpringListView。

通过上述代码,您可以为 RecyclerView 和 ListView 添加吸引人的弹簧边缘效果,从而提升应用程序的用户体验


注:本文转载自juejin.cn的望佑的文章"https://juejin.cn/post/7447843704495849498"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top