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


    效果图:

    前言:

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

    1. 布局文件 activity_main.xml

    1. "1.0" encoding="utf-8"?>
    2. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3. xmlns:app="http://schemas.android.com/apk/res-auto"
    4. xmlns:tools="http://schemas.android.com/tools"
    5. android:layout_width="match_parent"
    6. android:layout_height="match_parent"
    7. tools:context=".MainActivity">
    8. <com.example.flingscrollview.LinerScrollView
    9. android:id="@+id/mScrollView"
    10. android:orientation="vertical"
    11. android:layout_height="match_parent"
    12. android:layout_width="match_parent"/>
    13. FrameLayout>

    2. 演示View

    1. package com.example.flingscrollview;
    2. import android.content.Context;
    3. import android.graphics.Color;
    4. import android.os.Handler;
    5. import android.os.Looper;
    6. import android.util.AttributeSet;
    7. import android.view.Gravity;
    8. import android.view.MotionEvent;
    9. import android.view.VelocityTracker;
    10. import android.view.View;
    11. import android.view.ViewConfiguration;
    12. import android.view.ViewGroup;
    13. import android.widget.LinearLayout;
    14. import android.widget.TextView;
    15. import androidx.annotation.Nullable;
    16. public class LinerScrollView extends LinearLayout {
    17. final Handler mHandler;
    18. private final int mTouchSlop; // 移动的距离大于这个像素值的时候,会认为是在滑动
    19. private final int mMinimumVelocity; // 最小的速度
    20. private final int mMaximumVelocity; // 最大的速度
    21. private VelocityTracker mVelocityTracker; // 速度跟踪器
    22. private int mScrollPointerId; // 当前最新放在屏幕伤的手指
    23. private int mLastTouchX; // 上一次触摸的X坐标
    24. private int mLastTouchY; // 上一次触摸的Y坐标
    25. private int mInitialTouchX; // 初始化触摸的X坐标
    26. private int mInitialTouchY; // 初始化触摸的Y坐标
    27. public final int SCROLL_STATE_IDLE = -1; // 没有滚动
    28. public final int SCROLL_STATE_DRAGGING = 1; // 被手指拖动情况下滚动
    29. public final int SCROLL_STATE_SETTLING = 2; // 没有被手指拖动情况下,惯性滚动
    30. private int mScrollState = SCROLL_STATE_IDLE; // 滚动状态
    31. // 在测试过程中,通过速度正负值判断方向,方向有概率不准确
    32. // 所以我在onTouchEvent里自己处理
    33. private boolean direction = true; // true:向上 false:向下
    34. private FlingTask flingTask; // 惯性任务
    35. public LinerScrollView(Context context, @Nullable AttributeSet attrs) {
    36. super(context, attrs);
    37. mHandler = new Handler(Looper.getMainLooper());
    38. // 一些系统的预定义值:
    39. ViewConfiguration configuration = ViewConfiguration.get(getContext());
    40. mTouchSlop = configuration.getScaledTouchSlop();
    41. mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
    42. mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    43. initView();
    44. }
    45. /**
    46. * 初始化视图
    47. */
    48. private void initView() {
    49. for (int i = 0; i < 50; i++) {
    50. TextView textView = new TextView(getContext());
    51. ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 350);
    52. textView.setLayoutParams(params);
    53. textView.setText("index:" + i);
    54. textView.setTextColor(Color.BLACK);
    55. textView.setTextSize(30);
    56. textView.setBackgroundColor(Color.CYAN);
    57. textView.setGravity(Gravity.CENTER_VERTICAL);
    58. addView(textView);
    59. }
    60. }
    61. boolean notUp = false; // 是否 不能再向上滑了
    62. boolean notDown = false; // 是否 不能再向下滑了
    63. int listMaxOffsetY = 0; // 列表最大滑动Y值
    64. /**
    65. * 滚动列表
    66. * @param offsetY 偏移Y值
    67. */
    68. private void translationViewY(int offsetY) {
    69. if (listMaxOffsetY == 0) {
    70. listMaxOffsetY = (350 * 50) - getHeight();
    71. }
    72. if (mScrollState == SCROLL_STATE_DRAGGING) {
    73. if (direction) { // 向上滑动
    74. if (Math.abs(getChildAt((getChildCount() - 1)).getTranslationY()) < listMaxOffsetY) {
    75. notUp = false;
    76. }
    77. } else { // 向下滑动
    78. if (getChildAt(0).getTranslationY() < 0) {
    79. notDown = false;
    80. }
    81. }
    82. }
    83. for (int i = 0; i < getChildCount(); i++) {
    84. View childView = getChildAt(i);
    85. int yv = (int) (childView.getTranslationY() + offsetY);
    86. if (direction) { // 向上滑动
    87. notDown = false;
    88. if (!notUp) {
    89. if (Math.abs(yv) >= listMaxOffsetY) {
    90. notUp = true;
    91. }
    92. }
    93. if (!notUp) childView.setTranslationY(yv);
    94. } else { // 向下滑动
    95. notUp = false;
    96. if (!notDown) {
    97. if (yv >= 0) {
    98. notDown = true;
    99. }
    100. }
    101. if (!notDown) childView.setTranslationY(yv);
    102. }
    103. }
    104. }
    105. /**
    106. * 惯性任务
    107. * @param velocityX X轴速度
    108. * @param velocityY Y轴速度
    109. * @return
    110. */
    111. private boolean fling(int velocityX, int velocityY) {
    112. if (Math.abs(velocityY) > mMinimumVelocity) {
    113. flingTask = new FlingTask(Math.abs(velocityY), mHandler, new FlingTask.FlingTaskCallback() {
    114. @Override
    115. public void executeTask(int dy) {
    116. if (direction) { // 向上滑动
    117. translationViewY(-dy);
    118. } else { // 向下滑动
    119. translationViewY(dy);
    120. }
    121. }
    122. @Override
    123. public void stopTask() {
    124. setScrollState(SCROLL_STATE_IDLE);
    125. }
    126. });
    127. flingTask.run();
    128. setScrollState(SCROLL_STATE_SETTLING);
    129. return true;
    130. }
    131. return false;
    132. }
    133. /**
    134. * 停止惯性滚动任务
    135. */
    136. private void stopFling() {
    137. if (mScrollState == SCROLL_STATE_SETTLING) {
    138. if (flingTask != null) {
    139. flingTask.stopTask();
    140. setScrollState(SCROLL_STATE_IDLE);
    141. }
    142. }
    143. }
    144. @Override
    145. public boolean onTouchEvent(MotionEvent event) {
    146. super.onTouchEvent(event);
    147. boolean eventAddedToVelocityTracker = false;
    148. // 获取一个新的VelocityTracker对象来观察滑动的速度
    149. if (mVelocityTracker == null) {
    150. mVelocityTracker = VelocityTracker.obtain();
    151. }
    152. mVelocityTracker.addMovement(event);
    153. // 返回正在执行的操作,不包含触摸点索引信息。即事件类型,如MotionEvent.ACTION_DOWN
    154. final int action = event.getActionMasked();
    155. int actionIndex = event.getActionIndex();// Action的索引
    156. // 复制事件信息创建一个新的事件,防止被污染
    157. final MotionEvent copyEv = MotionEvent.obtain(event);
    158. switch (action) {
    159. case MotionEvent.ACTION_DOWN: { // 手指按下
    160. stopFling();
    161. // 特定触摸点相关联的触摸点id,获取第一个触摸点的id
    162. mScrollPointerId = event.getPointerId(0);
    163. // 记录down事件的X、Y坐标
    164. mInitialTouchX = mLastTouchX = (int) (event.getX() + 0.5f);
    165. mInitialTouchY = mLastTouchY = (int) (event.getY() + 0.5f);
    166. }
    167. break;
    168. case MotionEvent.ACTION_POINTER_DOWN: { // 多个手指按下
    169. // 更新mScrollPointerId,表示只会响应最近按下的手势事件
    170. mScrollPointerId = event.getPointerId(actionIndex);
    171. // 更新最近的手势坐标
    172. mInitialTouchX = mLastTouchX = (int) (event.getX() + 0.5f);
    173. mInitialTouchY = mLastTouchY = (int) (event.getY() + 0.5f);
    174. }
    175. break;
    176. case MotionEvent.ACTION_MOVE: { // 手指移动
    177. setScrollState(SCROLL_STATE_DRAGGING);
    178. // 根据mScrollPointerId获取触摸点下标
    179. final int index = event.findPointerIndex(mScrollPointerId);
    180. // 根据move事件产生的x,y来计算偏移量dx,dy
    181. final int x = (int) (event.getX() + 0.5f);
    182. final int y = (int) (event.getY() + 0.5f);
    183. int dx = Math.abs(mLastTouchX - x);
    184. int dy = Math.abs(mLastTouchY - y);
    185. // 在手指拖动状态下滑动
    186. if (mScrollState == SCROLL_STATE_DRAGGING) {
    187. if (mLastTouchY - y > 0.5f) {
    188. direction = true;
    189. // Log.d("TAG", "向上");
    190. translationViewY(-dy);
    191. } else if (y - mLastTouchY > 0.5f) {
    192. direction = false;
    193. // Log.d("TAG", "向下");
    194. translationViewY(dy);
    195. }
    196. }
    197. mLastTouchX = x;
    198. mLastTouchY = y;
    199. }
    200. break;
    201. case MotionEvent.ACTION_POINTER_UP: { // 多个手指离开
    202. // 选择一个新的触摸点来处理结局,重新处理坐标
    203. onPointerUp(event);
    204. }
    205. break;
    206. case MotionEvent.ACTION_UP: { // 手指离开,滑动事件结束
    207. mVelocityTracker.addMovement(copyEv);
    208. eventAddedToVelocityTracker = true;
    209. // 计算滑动速度
    210. mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    211. // 最后一次 X/Y 轴的滑动速度
    212. final float xVel = -mVelocityTracker.getXVelocity(mScrollPointerId);
    213. final float yVel = -mVelocityTracker.getYVelocity(mScrollPointerId);
    214. if (!((xVel != 0 || yVel != 0) && fling((int) xVel, (int) yVel))) {
    215. setScrollState(SCROLL_STATE_IDLE); // 设置滑动状态
    216. }
    217. resetScroll(); // 重置滑动
    218. }
    219. break;
    220. case MotionEvent.ACTION_CANCEL: { //手势取消,释放各种资源
    221. cancelScroll(); // 退出滑动
    222. }
    223. break;
    224. }
    225. if (!eventAddedToVelocityTracker) {
    226. // 回收滑动事件,方便重用,调用此方法你不能再接触事件
    227. mVelocityTracker.addMovement(copyEv);
    228. }
    229. // 回收滑动事件,方便重用
    230. copyEv.recycle();
    231. return true;
    232. }
    233. /**
    234. * 有新手指触摸屏幕,更新初始坐标
    235. * @param e
    236. */
    237. private void onPointerUp(MotionEvent e) {
    238. final int actionIndex = e.getActionIndex();
    239. if (e.getPointerId(actionIndex) == mScrollPointerId) {
    240. // Pick a new pointer to pick up the slack.
    241. final int newIndex = actionIndex == 0 ? 1 : 0;
    242. mScrollPointerId = e.getPointerId(newIndex);
    243. mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
    244. mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
    245. }
    246. }
    247. /**
    248. * 手指离开屏幕
    249. */
    250. private void cancelScroll() {
    251. resetScroll();
    252. setScrollState(SCROLL_STATE_IDLE);
    253. }
    254. /**
    255. * 重置速度
    256. */
    257. private void resetScroll() {
    258. if (mVelocityTracker != null) {
    259. mVelocityTracker.clear();
    260. }
    261. }
    262. /**
    263. * 更新 滚动状态
    264. * @param state
    265. */
    266. private void setScrollState(int state) {
    267. if (state == mScrollState) {
    268. return;
    269. }
    270. mScrollState = state;
    271. }
    272. }

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

    1. package com.example.flingscrollview;
    2. import android.os.Handler;
    3. import android.util.Log;
    4. /**
    5. * 惯性任务
    6. */
    7. public class FlingTask implements Runnable {
    8. private Handler mHandler;
    9. private int velocityY = 0;
    10. private int originalVelocityY = 0;
    11. private FlingTaskCallback flingTaskCallback;
    12. public FlingTask(int velocityY, Handler handler, FlingTaskCallback callback) {
    13. this.velocityY = velocityY;
    14. this.mHandler = handler;
    15. this.originalVelocityY = velocityY;
    16. this.flingTaskCallback = callback;
    17. }
    18. boolean initSlide = false; // 初始化滑动
    19. int average = 0; // 平均速度
    20. int tempAverage = 1;
    21. boolean startSmooth = false; // 开始递减速度平滑处理
    22. int sameCount = 0; // 值相同次数
    23. // 这里控制平均每段滑动的速度
    24. private int getAverageDistance(int velocityY) {
    25. int t = velocityY;
    26. if (t < 470) {
    27. t /= 21;
    28. }
    29. // divide by zero
    30. if (t == 0) return 0;
    31. int v = Math.abs(velocityY / t);
    32. if (v < 21) {
    33. t /= 21;
    34. if (t > 20) {
    35. t /= 5;
    36. }
    37. }
    38. return t;
    39. }
    40. @Override
    41. public void run() {
    42. // 速度完全消耗完才结束任务,和view滚动结束不冲突
    43. // 这个判断是为了扩展,将没消耗完的速度,转给指定的滚动view
    44. // if (velocityY > 0) {
    45. // 只要view滚动结束,立刻结束任务
    46. if (tempAverage > 0 && velocityY > 0) {
    47. if (!initSlide) {
    48. average = getAverageDistance(velocityY);
    49. initSlide = true;
    50. }
    51. float progress = (float) velocityY / originalVelocityY;
    52. float newProgress = 0f;
    53. if (average > 300) {
    54. newProgress = getInterpolation(progress);
    55. } else {
    56. newProgress = getInterpolation02(progress);
    57. }
    58. int prTemp = tempAverage;
    59. if (!startSmooth) tempAverage = (int) (average * newProgress);
    60. // 递减速度平滑处理
    61. if (prTemp == tempAverage) {
    62. sameCount++;
    63. if (sameCount > 1 && tempAverage > 0) { // 这个值越大,最后衰减停止时越生硬,0 - 30
    64. tempAverage--;
    65. sameCount = 0;
    66. startSmooth = true;
    67. }
    68. }
    69. flingTaskCallback.executeTask(tempAverage);
    70. velocityY -= tempAverage;
    71. // 这里这样写是为了扩展,将没消耗完的速度,转给其他滚动列表
    72. // 判断语句需要改成 if (velocityY > 0)
    73. if (tempAverage == 0) { // view滚动停止时
    74. // 如果速度没有消耗完,继续消耗
    75. flingTaskCallback.executeConsumptionTask(velocityY);
    76. velocityY -= average;
    77. }
    78. // Log.d("TAG", "tempAverage:" + tempAverage + " --- velocityY:" + velocityY + " --- originalVelocityY:" + originalVelocityY);
    79. mHandler.post(this);
    80. } else {
    81. flingTaskCallback.stopTask();
    82. stopTask();
    83. }
    84. }
    85. public void stopTask() {
    86. mHandler.removeCallbacks(this);
    87. initSlide = false;
    88. startSmooth = false;
    89. }
    90. // 从加速度到逐步衰减(AccelerateDecelerateInterpolator插值器 核心源码)
    91. public float getInterpolation(float input) {
    92. return (float) (Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    93. }
    94. // 速度逐步衰减(DecelerateInterpolator插值器 核心源码)
    95. public float getInterpolation02(float input) {
    96. return (float) (1.0f - (1.0f - input) * (1.0f - input));
    97. }
    98. public interface FlingTaskCallback {
    99. void executeTask(int dy);
    100. void stopTask();
    101. void executeConsumptionTask(int dy);
    102. }
    103. }

    4. Activity

    1. package com.example.flingscrollview;
    2. import androidx.appcompat.app.AppCompatActivity;
    3. import android.graphics.Color;
    4. import android.os.Bundle;
    5. import android.view.View;
    6. import android.view.ViewGroup;
    7. import android.widget.TextView;
    8. public class MainActivity extends AppCompatActivity {
    9. @Override
    10. protected void onCreate(Bundle savedInstanceState) {
    11. super.onCreate(savedInstanceState);
    12. setContentView(R.layout.activity_main);
    13. }
    14. }
  • 相关阅读:
    LP10备料单指定WM仓位设定(生产供应区域设定)
    执行SQL语句&存储过程的真正【神器】,不用ORM的全选它,比dapper好
    近200篇文章汇总而成的机器翻译非自回归生成最新综述,揭示其挑战和未来研究方向...
    APS成功实施的关键要点
    前端 JS 经典:上传文件
    适合上班族使用的电脑笔记软件使用哪一款
    【Redis 系列】redis 学习十六,redis 字典(map) 及其核心编码结构
    道可云元宇宙每日资讯|《江苏省元宇宙产业发展行动计划》发布
    【数据结构】图的存储结构—邻接表
    铭飞MCms不建议使用
  • 原文地址:https://blog.csdn.net/Lan_Se_Tian_Ma/article/details/134323084