Android自定义 View惯性滚动效果(不使用Scroller)

news2024/11/26 8:21:19

效果图:

前言:

看了网上很多惯性滚动方案,都是通过Scroller 配合 computeScroll实现的,但在实际开发中可能有一些场景不合适,比如协调布局,内部子View有特别复杂的联动效果,需要通过偏移来配合。我通过VelocityTracker(速度跟踪器)实现了相同的效果,感觉还行🤣,欢迎指正,虚拟机有延迟,真机效果更好。

1. 布局文件 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.flingscrollview.LinerScrollView
        android:id="@+id/mScrollView"
        android:orientation="vertical"
        android:layout_height="match_parent"
        android:layout_width="match_parent"/>

</FrameLayout>

2. 演示View

package com.example.flingscrollview;

import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.Nullable;

public class LinerScrollView extends LinearLayout {

    final Handler mHandler;
    private final int mTouchSlop; // 移动的距离大于这个像素值的时候,会认为是在滑动
    private final int mMinimumVelocity; // 最小的速度
    private final int mMaximumVelocity; // 最大的速度
    private VelocityTracker mVelocityTracker; // 速度跟踪器
    private int mScrollPointerId; // 当前最新放在屏幕伤的手指
    private int mLastTouchX; // 上一次触摸的X坐标
    private int mLastTouchY; // 上一次触摸的Y坐标
    private int mInitialTouchX; // 初始化触摸的X坐标
    private int mInitialTouchY; // 初始化触摸的Y坐标
    public final int SCROLL_STATE_IDLE = -1; // 没有滚动
    public final int SCROLL_STATE_DRAGGING = 1; // 被手指拖动情况下滚动
    public final int SCROLL_STATE_SETTLING = 2; // 没有被手指拖动情况下,惯性滚动
    private int mScrollState = SCROLL_STATE_IDLE; // 滚动状态

    // 在测试过程中,通过速度正负值判断方向,方向有概率不准确
    // 所以我在onTouchEvent里自己处理
    private boolean direction = true; // true:向上 false:向下
    private FlingTask flingTask; // 惯性任务

    public LinerScrollView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mHandler = new Handler(Looper.getMainLooper());

        // 一些系统的预定义值:
        ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();

        initView();
    }

    /**
     * 初始化视图
     */
    private void initView() {
        for (int i = 0; i < 50; i++) {
            TextView textView = new TextView(getContext());
            ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 350);
            textView.setLayoutParams(params);
            textView.setText("index:" + i);
            textView.setTextColor(Color.BLACK);
            textView.setTextSize(30);
            textView.setBackgroundColor(Color.CYAN);
            textView.setGravity(Gravity.CENTER_VERTICAL);
            addView(textView);
        }
    }

    boolean notUp = false; // 是否 不能再向上滑了
    boolean notDown = false; // 是否 不能再向下滑了
    int listMaxOffsetY = 0; // 列表最大滑动Y值

    /**
     * 滚动列表
     * @param offsetY 偏移Y值
     */
    private void translationViewY(int offsetY) {
        if (listMaxOffsetY == 0) {
            listMaxOffsetY = (350 * 50) - getHeight();
        }

        if (mScrollState == SCROLL_STATE_DRAGGING) {

            if (direction) { // 向上滑动
                if (Math.abs(getChildAt((getChildCount() - 1)).getTranslationY()) < listMaxOffsetY) {
                    notUp = false;
                }
            } else { // 向下滑动
                if (getChildAt(0).getTranslationY() < 0) {
                    notDown = false;
                }
            }
        }

        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            int yv = (int) (childView.getTranslationY() + offsetY);
            if (direction) { // 向上滑动
                notDown = false;
                if (!notUp) {
                    if (Math.abs(yv) >= listMaxOffsetY) {
                        notUp = true;
                    }
                }
                if (!notUp) childView.setTranslationY(yv);
            } else { // 向下滑动
                notUp = false;
                if (!notDown) {
                    if (yv >= 0) {
                        notDown = true;
                    }
                }
                if (!notDown) childView.setTranslationY(yv);
            }
        }
    }

    /**
     * 惯性任务
     * @param velocityX X轴速度
     * @param velocityY Y轴速度
     * @return
     */
    private boolean fling(int velocityX, int velocityY) {
        if (Math.abs(velocityY) > mMinimumVelocity) {
            flingTask = new FlingTask(Math.abs(velocityY), mHandler, new FlingTask.FlingTaskCallback() {
                @Override
                public void executeTask(int dy) {
                    if (direction) { // 向上滑动
                        translationViewY(-dy);
                    } else { // 向下滑动
                        translationViewY(dy);
                    }
                }

                @Override
                public void stopTask() {
                    setScrollState(SCROLL_STATE_IDLE);
                }
            });

            flingTask.run();
            setScrollState(SCROLL_STATE_SETTLING);
            return true;
        }
        return false;
    }

    /**
     * 停止惯性滚动任务
     */
    private void stopFling() {
        if (mScrollState == SCROLL_STATE_SETTLING) {
            if (flingTask != null) {
                flingTask.stopTask();
                setScrollState(SCROLL_STATE_IDLE);
            }
        }
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        boolean eventAddedToVelocityTracker = false;

        // 获取一个新的VelocityTracker对象来观察滑动的速度
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        // 返回正在执行的操作,不包含触摸点索引信息。即事件类型,如MotionEvent.ACTION_DOWN
        final int action = event.getActionMasked();
        int actionIndex = event.getActionIndex();// Action的索引

        // 复制事件信息创建一个新的事件,防止被污染
        final MotionEvent copyEv = MotionEvent.obtain(event);

        switch (action) {
            case MotionEvent.ACTION_DOWN: { // 手指按下
                stopFling();

                // 特定触摸点相关联的触摸点id,获取第一个触摸点的id
                mScrollPointerId = event.getPointerId(0);

                // 记录down事件的X、Y坐标
                mInitialTouchX = mLastTouchX = (int) (event.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (event.getY() + 0.5f);
            }
            break;
            case MotionEvent.ACTION_POINTER_DOWN: { // 多个手指按下
                // 更新mScrollPointerId,表示只会响应最近按下的手势事件
                mScrollPointerId = event.getPointerId(actionIndex);

                // 更新最近的手势坐标
                mInitialTouchX = mLastTouchX = (int) (event.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (event.getY() + 0.5f);
            }
            break;
            case MotionEvent.ACTION_MOVE: { // 手指移动
                setScrollState(SCROLL_STATE_DRAGGING);

                // 根据mScrollPointerId获取触摸点下标
                final int index = event.findPointerIndex(mScrollPointerId);

                // 根据move事件产生的x,y来计算偏移量dx,dy
                final int x = (int) (event.getX() + 0.5f);
                final int y = (int) (event.getY() + 0.5f);

                int dx = Math.abs(mLastTouchX - x);
                int dy = Math.abs(mLastTouchY - y);

                // 在手指拖动状态下滑动
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    if (mLastTouchY - y > 0.5f) {
                        direction = true;
                        // Log.d("TAG", "向上");
                        translationViewY(-dy);
                    } else if (y - mLastTouchY > 0.5f) {
                        direction = false;
                        // Log.d("TAG", "向下");
                        translationViewY(dy);
                    }
                }

                mLastTouchX = x;
                mLastTouchY = y;
            }
            break;
            case MotionEvent.ACTION_POINTER_UP: { // 多个手指离开
                // 选择一个新的触摸点来处理结局,重新处理坐标
                onPointerUp(event);
            }
            break;
            case MotionEvent.ACTION_UP: { // 手指离开,滑动事件结束
                mVelocityTracker.addMovement(copyEv);
                eventAddedToVelocityTracker = true;

                // 计算滑动速度
                // mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);

                // 最后一次 X/Y 轴的滑动速度
                final float xVel = -mVelocityTracker.getXVelocity(mScrollPointerId);
                final float yVel = -mVelocityTracker.getYVelocity(mScrollPointerId);

                if (!((xVel != 0 || yVel != 0) && fling((int) xVel, (int) yVel))) {
                    setScrollState(SCROLL_STATE_IDLE); // 设置滑动状态
                }
                resetScroll(); // 重置滑动
            }
            break;
            case MotionEvent.ACTION_CANCEL: { //手势取消,释放各种资源
                cancelScroll(); // 退出滑动
            }
            break;
        }

        if (!eventAddedToVelocityTracker) {
            // 回收滑动事件,方便重用,调用此方法你不能再接触事件
            mVelocityTracker.addMovement(copyEv);
        }

        // 回收滑动事件,方便重用
        copyEv.recycle();
        return true;
    }

    /**
     * 有新手指触摸屏幕,更新初始坐标
     * @param e
     */
    private void onPointerUp(MotionEvent e) {
        final int actionIndex = e.getActionIndex();
        if (e.getPointerId(actionIndex) == mScrollPointerId) {
            // Pick a new pointer to pick up the slack.
            final int newIndex = actionIndex == 0 ? 1 : 0;
            mScrollPointerId = e.getPointerId(newIndex);
            mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
        }
    }

    /**
     * 手指离开屏幕
     */
    private void cancelScroll() {
        resetScroll();
        setScrollState(SCROLL_STATE_IDLE);
    }

    /**
     * 重置速度
     */
    private void resetScroll() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
        }
    }

    /**
     * 更新 滚动状态
     * @param state
     */
    private void setScrollState(int state) {
        if (state == mScrollState) {
            return;
        }
        mScrollState = state;
    }

}

3. 惯性滚动任务类(核心类)

package com.example.flingscrollview;

import android.os.Handler;
import android.util.Log;

class FlingTask implements Runnable {

    private Handler mHandler;
    private int velocityY = 0;
    private int originalVelocityY = 0;
    private FlingTaskCallback flingTaskCallback;

    public FlingTask(int velocityY, Handler handler, FlingTaskCallback callback) {
        this.velocityY = velocityY;
        this.mHandler = handler;
        this.originalVelocityY = velocityY;
        this.flingTaskCallback = callback;
    }

    boolean initSlide = false; // 初始化滑动
    int average = 0; // 平均速度
    int tempAverage = 1;
    boolean startSmooth = false; // 开始递减速度平滑处理
    int sameCount = 0; // 值相同次数

    // 这里控制平均每段滑动的速度
    private int getAverageDistance(int velocityY) {
        int t = velocityY;
        if (t < 470) {
            t /= 21;
        }
        // divide by zero
        if (t == 0) return 0;
        int v = Math.abs(velocityY / t);
        if (v < 21) {
            t /= 21;
            if (t > 20) {
                t /= 5;
            }
        }
        return t;
    }

    @Override
    public void run() {
        // 速度完全消耗完才结束任务,和view滚动结束不冲突
        // 这个判断是为了扩展,将没消耗完的速度,转给指定的滚动view
        // if (velocityY > 0) {

        // 只要view滚动结束,立刻结束任务
        if (tempAverage > 0 && velocityY > 0) {

            if (!initSlide) {
                average = getAverageDistance(velocityY);
                initSlide = true;
            }

            float progress = (float) velocityY / originalVelocityY;
            float newProgress = 0f;
            if (average > 300) {
                newProgress = getInterpolation(progress);
            } else {
                newProgress = getInterpolation02(progress);
            }

            int prTemp = tempAverage;
            if (!startSmooth) tempAverage = (int) (average * newProgress);

            // 递减速度平滑处理
            if (prTemp == tempAverage) {
                sameCount++;
                if (sameCount > 1 && tempAverage > 0) { // 这个值越大,最后衰减停止时越生硬,0 - 30
                    tempAverage--;
                    sameCount = 0;
                    startSmooth = true;
                }
            }

            flingTaskCallback.executeTask(tempAverage);

            velocityY -= tempAverage;

            // 这里这样写是为了扩展,将没消耗完的速度,转给其他滚动列表
            // 判断语句需要改成 if (velocityY > 0)
            if (tempAverage == 0) { // view滚动停止时
                // 如果速度没有消耗完,继续消耗
                velocityY -= average;
            }
            // Log.d("TAG", "tempAverage:" + tempAverage + " --- velocityY:" + velocityY + " --- originalVelocityY:" + originalVelocityY);

            mHandler.post(this);
        } else {
            flingTaskCallback.stopTask();
            stopTask();
        }
    }

    public void stopTask() {
        mHandler.removeCallbacks(this);
        initSlide = false;
    }


    // 从加速度到逐步衰减(AccelerateDecelerateInterpolator插值器 核心源码)
    public float getInterpolation(float input) {
        return (float) (Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }

    // 速度逐步衰减(DecelerateInterpolator插值器 核心源码)
    public float getInterpolation02(float input) {
        return (float) (1.0f - (1.0f - input) * (1.0f - input));
    }

    interface FlingTaskCallback {
        void executeTask(int dy);

        void stopTask();
    }
}

4. Activity

package com.example.flingscrollview;

import androidx.appcompat.app.AppCompatActivity;

import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1190828.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript从入门到精通系列第三十二篇:详解正则表达式语法(一)

文章目录 一&#xff1a;正则表达式 1&#xff1a;量词设置次数 2&#xff1a;检查字符串以什么开头 3&#xff1a;检查字符串以什么结尾 4&#xff1a; 同时使用开头结尾 5&#xff1a;同值开头同值结尾 二&#xff1a;练习 1&#xff1a;检查是否是一个手机号 大神链…

(附源码)基于Springboot智慧园区管理系统-计算机毕设 88160

Springboot智慧园区管理系统的开发 摘要 随着互联网趋势的到来&#xff0c;互联网概念越来越盛行&#xff0c;园区管理最好方式就是建立自己的互联网系统。在现实运用中&#xff0c;应用软件的工作规则和开发步骤&#xff0c;采用Springboot框架建设智慧园区管理系统。 本设计主…

线性代数(五) | 矩阵对角化 特征值 特征向量

文章目录 1 矩阵的特征值和特征向量究竟是什么&#xff1f;2 求特征值和特征向量3 特征值和特征向量的应用4 矩阵的对角化 1 矩阵的特征值和特征向量究竟是什么&#xff1f; 矩阵实际上是一种变换,是一种旋转伸缩变换&#xff08;方阵&#xff09; 不是方阵的话还有可能是一种…

博阳精讯、凡得科技访问上海斯歌:共探BPM流程服务新高地

10月27日下午&#xff0c;来自博阳精讯、凡得科技的流程领域专家、领导一行参观访问了上海斯歌总部。三方举行了深度交流会谈&#xff0c;分享了彼此对流程领域的前沿洞察和技术实践&#xff0c;共同探索了BPM流程服务科技力与价值力的新高地。 本次研讨会上&#xff0c;博阳精…

WPF 线程模型

Windows Presentation Foundation (WPF) 旨在将开发人员从线程处理困难中解脱出来。 因此&#xff0c;大多数 WPF 开发人员不会编写使用多个线程的界面。 由于多线程程序既复杂又难以调试&#xff0c;因此当存在单线程解决方案时&#xff0c;应避免使用多线程程序。 但是&…

【Docker安装RockeMQ:基于Windows宿主机,并重点解决docker rocketMQ安装情况下控制台无法访问的问题】

拉取镜像 docker pull rocketmqinc/rocketmq创建网络 docker network create rocketmq-net构建namesrv容器 docker run -d -p 9876:9876 -v D:/dockerFile/rocketmq/namesrv/logs:/root/logs -v D:/dockerFile/rocketmq/namesrv/store:/root/store --network rocketmq-net -…

11.9存储器实验总结(单ram,双ram,FIFO)

实验设计 单端口RAM实现 双端口RAM实现 FIFO实现 文件结构为

【Royalty in Wind 2.0.0】个人体测计算、资料分享小程序

前言 Royalty in Wind 是我个人制作的一个工具类小程序。主要涵盖体测计算器、个人学习资料分享等功能。这个小程序在2022年第一次发布&#xff0c;不过后来因为一些原因暂时搁置。现在准备作为我个人的小程序重新投入使用XD PS&#xff1a;小程序开发部分我是在21年跟随郄培…

使命担当 守护安全 | 中睿天下获全国海关信息中心感谢信

近日&#xff0c;全国海关信息中心向中睿天下发来感谢信&#xff0c;对中睿天下在2023年网络攻防演练专项活动中的大力支持和优异表现给予了高度赞扬。 中睿天下对此次任务高度重视&#xff0c;紧密围绕全国海关信息中心的行动要求&#xff0c;发挥自身优势有效整合资源&#x…

Spring Boot 3.0正式发布及新特性解读

目录 【1】Spring Boot 3.0正式发布及新特性依赖调整升级的关键变更支持 GraalVM 原生镜像 Spring Boot 最新支持版本Spring Boo 版本版本 3.1.5前置系统清单三方包升级 Ref 个人主页: 【⭐️个人主页】 需要您的【&#x1f496; 点赞关注】支持 &#x1f4af; 【1】Spring Boo…

GUI:贪吃蛇

以上是准备工作 Data import javax.swing.*; import java.net.URL;public class Data {public static URL headerURLData.class.getResource("static/header.png");public static ImageIcon header new ImageIcon(headerURL);public static URL upURLData.class.getR…

【树的存储结构,孩子链表】

文章目录 树和森林树的存储结构孩子链表 树和森林 森林&#xff1a;是m(m>0)棵互不相交的树的集合。 树的存储结构 1.双亲表示法 实现&#xff1a;定义结构数组存放树的结点&#xff0c;每个结点含两个域。 数据域&#xff1a;存放结点本身信息。 双亲域&#xff1a;指…

如何设计vue项目的权限管理?

权限管理的重要性及必要性 数据安全&#xff1a;权限管理可以确保只有具有相应权限的用户能够访问和操作特定的数据。这可以保护敏感数据不被未授权的用户访问&#xff0c;从而提高数据的安全性。功能控制&#xff1a;权限管理可以根据用户的角色和权限设置&#xff0c;控制用户…

Ansible自动化运维工具(常用模块与命令)

ansible基于Python开发&#xff0c;实现了批量系统配置&#xff0c;批量程序部署&#xff0c;批量运行命令等功能 ansible特点 部署简单&#xff0c;只需在主控端部署Ansible环境&#xff0c;被控端无需做任何操作&#xff1b;默认使用ssh协议对设备进行管理&#xff1b;有大…

SPASS-交叉表分析

导入数据 修改变量测量类型 分析->描述统计->交叉表 表中显示行、列变量通过卡方检验给出的独立性检验结果。共使用了三种检验方法。上表各种检验方法显著水平sig.都远远小于0.05,所以有理由拒绝实验准备与评价结果是独立的假设&#xff0c;即认为实验准备这个评价指标是…

DehazeNet: An End-to-End System for Single Image Haze Removal(端到端的去雾模型)

1、论文去雾总体思路 DehazeNet是2016年华南理工大学的研究者提出的一个端到端的深度学习模型&#xff0c;该模型主要通过输入的原始有雾图像拟合出该图所对应的medium transmission map&#xff08;透射率t值图&#xff09;&#xff0c;并使用引导滤波对t值进行refine&#x…

SpringCloud - OpenFeign 参数传递和响应处理(全网最详细)

目录 一、OpenFeign 参数传递和响应处理 1.1、feign 客户端参数传递 1.1.1、零散类型参数传递 1. 例如 querystring 方式传参 2. 例如路径方式传参 1.1.2、对象参数传递 1. 对象参数传递案例 1.1.3、数组参数传递 1. 数组传参案例 1.1.4、集合类型的参数传递&#xf…

PHP+MySQL人才招聘小程序系统源码 带完整前端+后端搭建教程

在当今竞争激烈的人才市场中&#xff0c;招聘平台的需求日益增长。传统的招聘平台往往需要投入大量的人力物力进行维护和管理&#xff0c;这对于许多中小企业来说是一个沉重的负担。因此&#xff0c;开发一个简单易用、高效便捷的招聘平台显得尤为重要。 PHP是一种流行的服务器…

通过docker-compose部署elk日志系统,并使用springboot整合

ELK是一种强大的分布式日志管理解决方案&#xff0c;它由三个核心组件组成&#xff1a; Elasticsearch&#xff1a;作为分布式搜索和分析引擎&#xff0c;Elasticsearch能够快速地存储、搜索和分析大量的日志数据&#xff0c;帮助用户轻松地找到所需的信息。 Logstash&#xf…

逐次变分模态分解(Sequential Variational Mode Decomposition,SVMD)(附代码)

代码原理 逐次变分模态分解&#xff08;Sequential Variational Mode Decomposition&#xff0c;SVMD&#xff09;是一种用于信号处理和数据分析的方法。它可以将复杂的信号分解为一系列模态函数&#xff0c;每个模态函数代表了信号中的一个特定频率成分。SVMD的主要目标是提取…