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

效果图:

前言:

看了网上很多惯性滚动方案,都是通过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.LinerScrollViewandroid: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() {@Overridepublic void executeTask(int dy) {if (direction) { // 向上滑动translationViewY(-dy);} else { // 向下滑动translationViewY(dy);}}@Overridepublic 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);}}}@Overridepublic boolean onTouchEvent(MotionEvent event) {super.onTouchEvent(event);boolean eventAddedToVelocityTracker = false;// 获取一个新的VelocityTracker对象来观察滑动的速度if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}mVelocityTracker.addMovement(event);// 返回正在执行的操作,不包含触摸点索引信息。即事件类型,如MotionEvent.ACTION_DOWNfinal int action = event.getActionMasked();int actionIndex = event.getActionIndex();// Action的索引// 复制事件信息创建一个新的事件,防止被污染final MotionEvent copyEv = MotionEvent.obtain(event);switch (action) {case MotionEvent.ACTION_DOWN: { // 手指按下stopFling();// 特定触摸点相关联的触摸点id,获取第一个触摸点的idmScrollPointerId = 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,dyfinal 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 zeroif (t == 0) return 0;int v = Math.abs(velocityY / t);if (v < 21) {t /= 21;if (t > 20) {t /= 5;}}return t;}@Overridepublic 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 - 30tempAverage--;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 {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Centos7安装配置中文输入法

Centos7安装配置中文输入法 在安装CentOS时&#xff0c;我们为了方便使用&#xff0c;语言选择了中文&#xff0c;但是我们发现&#xff0c;在Linux命令行或者是浏览器中输入时&#xff0c;我们只能输入英文&#xff0c;无法输入汉字。 来&#xff0c;跟随脚步&#xff0c;设…

【工具】OCR方法|不用下载额外的软件,提取扫描中英文PDF的目录文本的最优解!(一)

需求&#xff1a; 1&#xff09;从PDF里快速提取目录&#xff1b; 2&#xff09;不想下载任何软件。 我提取出来的目录文本会用于嵌入到PDF中&#xff0c;向PDF批量添加目录的软件以及软件的使用方法可以看我上一篇文章&#xff1a;PDF批量插入目录。 以下是我自己能想到的方…

智能AI系统ChatGPT系统源码+支持GPT4.0+支持ai绘画(Midjourney)/支持OpenAI GPT全模型+国内AI全模型

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

无人机红外相机的畸变矫正

在项目开展过程中&#xff0c;发现大疆M30T的红外相机存在比较明显的畸变问题&#xff0c;因此需要对红外图像进行畸变矫正。在资料检索过程中&#xff0c;发现对红外无人机影像矫正的资料较少&#xff0c;对此&#xff0c;我从相机的成像原理角度出发&#xff0c;探索出一种效…

基于安卓android微信小程序的校园互助平台

项目介绍 随着社会的发展&#xff0c;社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。 本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;它主要是采用java语言技术和mysql数据库来完成对系统的设计。整…

电销行业获客的精准客源从哪里来的?

在电话营销行业中找到精确的客户资源对电话营销的成功至关重要。 这里有几种方法可以找到准确的客户资源&#xff1a; 1、自身数据库&#xff1a;首先&#xff0c;使用现有的客户信息数据库&#xff0c;如客户电话号码、电子邮件和地址&#xff0c;来推广和营销现有客户。 2…

Mysql 不同存储引擎数据文件的形式详解

目录 MyISAM MERGE InnoDB Memory Archive CSV BLACKHOLE MySQL 中的每一个数据表在磁盘上至少被表示为一个文件&#xff0c;即存放着该数据表结构定义的 .frm 文件。不同的存储引擎还有其它用来存放数据和索引信息的文件。 从 MySQL 8.0 版本开始&#xff0c;frm 表结构…

「随笔」浅谈2023年云计算的发展趋势

在2023年&#xff0c;云计算的发展趋势将受到政治、经济、社会和科技四个维度的影响。以下是对这些维度的具体分析&#xff1a; 1.1 政治维度&#xff1a; 全球政策推动&#xff1a; 随着全球各国政策对云计算的重视程度不断提高&#xff0c;云计算服务将获得更广泛的市场准入…

[工业自动化-7]:西门子S7-15xxx编程 - PLC主站 - 电源模块

目录 前言&#xff1a; 一、主站电源PM VS PS 1.1 主站PM电源模块(PM) 1.2 主站PS电源模块 1.3 PM/PS电源模块区别 1.4 如何选择PM/PS电源 1.5 什么时候必须使用PM模块 1.6 什么时候必须使用PS模块 二、背板总线 三、电源模块的安装 前言&#xff1a; 一、主站电源PM…

后台管理系统解决方案-中大型-Vben Admin

后台管理系统解决方案-中大型-Vben Admin 官网 Vben Admin 在线演示 Vben Admin 为什么选择它 github现有20K星&#xff0c;并且它有个可视化生成表单&#xff0c;我很喜欢 快速开始 # 拉取代码 git clone https://github.com/vbenjs/vue-vben-admin-doc# 安装依赖 yarn#…

Effective C++ 系列和 C++ Core Guidelines 如何选择?

Effective C 系列和 C Core Guidelines 如何选择&#xff1f; 如果一定要二选一&#xff0c;我会选择C Core Guidelines。因为它是开源的&#xff0c;有300多个贡献者&#xff0c;而且还在不断更新&#xff0c;意味着它归纳总结了最新的C实践经验。最近很多小伙伴找我&#xff…

通过一道题目带你深入了解WAF特性、PHP超级打印函数、ASCII码chr()对应表等原理[RoarCTF 2019]Easy Calc 1

题目环境&#xff1a; 依此输入以下内容并查看回显结果 11 1’ index.php ls 到这里没思路了 F12查看源代码 一定要仔细看啊&#xff0c;差点没找到&#xff0c;笑哭 访问calc.php文件 果然有点东西 PHP代码审计 error_reporting(0);关闭错误报告 通过GET方式传参的参数num sho…

基于SSM的演唱会购票系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue、HTML 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是…

AcWing99. 激光炸弹

题目 地图上有 N N N 个目标&#xff0c;用整数 X i , Y i X_i,Y_i Xi​,Yi​ 表示目标在地图上的位置&#xff0c;每个目标都有一个价值 W i W_i Wi​。 注意&#xff1a;不同目标可能在同一位置。 现在有一种新型的激光炸弹&#xff0c;可以摧毁一个包含 R R RR RR 个…

动态规划(3)---Leetcode509.斐波那契数

题目 分析 很明显的动态规划&#xff0c;直接写出。之前都是用递归来写。 题解 class Solution {public int fib(int n) {if (n0) return 0;if (n1) return 1;int q0,p1,r0;for(int i2;i<n;i){rqp;int tmpp;pr;qtmp; }return r;}

快速教程|如何在 AWS EC2上使用 Walrus 部署 GitLab

Walrus 是一款基于平台工程理念的开源应用管理平台&#xff0c;致力于解决应用交付领域的深切痛点。借助 Walrus 将云原生的能力和最佳实践扩展到非容器化环境&#xff0c;并支持任意应用形态统一编排部署&#xff0c;降低使用基础设施的复杂度&#xff0c;为研发和运维团队提供…

移远EC600U-CN开发板 day01

1.官方文档快速上手&#xff0c;安装驱动&#xff0c;下载QPYcom QuecPython 快速入门 - QuecPython (quectel.com)https://python.quectel.com/doc/Getting_started/zh/index.html 注意&#xff1a; &#xff08;1&#xff09;打开开发板步骤 成功打开之后就可以连接开发板…

“2024杭州国际物联网展览会”定于4月份在杭州国际博览中心召开

随着科技的飞速发展&#xff0c;物联网已经成为当今社会的一个重要组成部分。物联网技术正在逐渐渗透到各个领域&#xff0c;为人们的生活带来更多的便利和智慧。物联网的发展趋势将受到技术、应用、安全等多方面的影响和推动。未来&#xff0c;物联网将更加智能化、自主化和安…

《开箱元宇宙》:认识香港麦当劳通过 The Sandbox McNuggets Land 的 Web3 成功经验

McNuggets Land 是 The Sandbox 于 2023 年发布的最受欢迎的体验之一。在本期的《开箱元宇宙》系列中&#xff0c;我们采访了香港麦当劳数位顾客体验暨合作伙伴资深总监 Kai Tsang&#xff0c;来了解这一成功案例背后的策略。 在不断发展的市场营销和品牌推广领域&#xff0c;不…

运行springboot时提示:源值 7 已过时,将在未来版本中删除,并且提示java.time not exist, LocaDateTime类找不到。

运行springboot时提示&#xff1a;源值 7 已过时&#xff0c;将在未来版本中删除&#xff0c;并且提示 java.time not exist, LocaDateTime类找不到。 解决方法&#xff1a; 方式一&#xff1a;通过IDEA修改这几个地方的JDK版本 1&#xff09;打开ProjectStructure->Proj…