物理学的 H5 应用:模拟惯性滑动

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了物理学的 H5 应用:模拟惯性滑动相关的知识,希望对你有一定的参考价值。

参考技术A

在移动端 H5 中,时间选择器( date-picker )、省市区选择器( area-picker )等组件经常会使用这样的交互效果:

这个 gif 是在【微信钱包 - 账单】中录制的 ios 原生时间选择器。可见, 当用户手指在选择器上先是滑动再从屏幕上移开,内容会继续保持一段时间的滚动效果,并且滚动的速度和持续的时间是与滑动手势的强烈程度成正比。 这种交互思路源于 ios 系统原生元素的滚动回弹( momentum-based scrolling ),来看 H5 的一个普通列表在 ios 上的滚动表现:

社区上大部分的移动端组件库的选择器组件都采取了这种交互方式,看看效果:

weui 的选择器实现了惯性滑动,但滑动动画结束得有点突兀,效果一般。

vant 的选择器压根没有做惯性滑动,当手指从屏幕上移开后,选择器的滑动会立刻停止。可见这样的交互体验是比较差的。

接下来我会从设计层面剖析和模拟惯性滑动的交互效果。

不难想象,惯性滑动非常贴合现实生活中的一些场景,如汽车刹车等。除此之外,与物理力学中的滑块模型也十分相似,由此我会参考滑块模型来剖析惯性滑动的全过程。

惯性 来源于物理学中的惯性定律(即 牛顿第一定律 ):一切物体在没有受到力的作用的时候,运动状态不会发生改变,物体所拥有的这种性质就被称为惯性。我们不妨把惯性滑动模拟成滑动滑块然后释放的过程(以下讨论中用户滑动的目标皆模拟成 滑块 ),主要划分为两个阶段:

描述滑块的惯性滑动,首先需要求出滑动的距离。在上述二阶段中,滑块受摩擦力 作 匀减速直线运动 。假设滑动距离为 ,初速度为 ,末速度为 。根据位移公式

加速度公式

可以算出惯性滑动距离

由于匀减速运动的加速度为负,不妨设一个加速度常量 ,使其满足 ,那么

这里 为正数。也就是说,我们只需要求出初始速度即可。

关注第一个阶段,假设用户滑动滑块的距离为 ,滑动的持续时间是 ,那么二阶段的初速度 可以根据位移公式求得

综上,求惯性滑动的距离我们需要记录用户滑动滑块的 距离 持续时间 ,并设置一个合理的 加速度常量

注意,这里的距离和持续时间并不是用户滑动滑块的总距离和时长,而是触发惯性滑动范围内的距离和时长,详见【惯性滑动的启动条件】。

针对二阶段的匀减速直线运动,时间段 产生的位移差 ,其中 。也就是说时间越往后,同等时间间距下通过的位移越来越小,也就是动画的推进速度越来越慢。

这与 CSS3 transition-timing-function 中的 ease-out 速度曲线相吻合, ease-out (即 cubic-bezier(0, 0, .58, 1) )的贝塞尔曲线为

上图来自 在线绘制贝塞尔曲线网站 。图表中的纵坐标是指 动画推进的进程 ;横坐标是指 时间 ;原点坐标为 (0, 0) ,终点坐标为 (1, 1) ,假设动画持续时间为2秒, (1, 1) 坐标点则代表离动画开始2秒时动画执行完毕(100%)。根据图表可以得出,时间越往后动画进程的推进速度越慢,符合匀减速直线运动的特性。

然而这样的速度曲线过于线性平滑,减速效果不明显。我们基于 ios 滚动回弹的效果,调整贝塞尔曲线的参数为 cubic-bezier(.17, .89, .45, 1) 。

滑块滑动不是无边界的,我们来考虑这样的场景:当滑块向下滑动,其顶部正要接触容器上边界时速度还没有降到 ,此时如果让滑块瞬间停止运动,这样的交互效果是不理想的。

我们可以把上边界想象成一条与滑块紧密贴合的固定弹簧, 当滑块到达临界点而速度还没有降到 时,滑块会继续滑动并拉动弹簧使其往下形变,同时会受到弹簧的反拉力作减速运动(动能转化为内能);当滑块速度降为 ,此时弹簧的形变量最大,由于弹性特质弹簧会恢复原状(内能转化成动能),从而拉动滑块反向运动

回弹过程也可以分为两个阶段:

根据上述分析,回弹的第一阶段作加速度越来越大的变减速直线运动,设此阶段的初速度为 ,可以与 建立以下关系

那么回弹距离为

微积分都来了,简直没法算好吧…

我们可以根据运动模型来简化 的计算,由于该阶段的加速度大于 非回弹惯性滑动 的加速度,设 非回弹惯性滑动 的总距离为 ,那么

所以可以设置一个合理的常量 ,使其满足

整个触发回弹的惯性滑动模型包括三个运动阶段:

然而把 阶段a 和 阶段b 描绘成 CSS 动画是有一定复杂度和风险的:

出于简化的考虑,可以将 阶段a、b 合并为一个运动阶段:

对于合并后的 阶段a 末段,由于反向加速度越来越大,因此滑块减速的效率会比 非回弹惯性滑动 同期更大,对应的贝塞尔曲线末段也会更陡,参数调整为 cubic-bezier(.25, .46, .45, .94) 。

在 阶段b 中,滑块先变加速后变减速,尝试 ease-in-out 的动画曲线:

可以看出,由于 阶段b 初始的 ease-in 曲线使 阶段a、b 的衔接段稍有停留,效果体验一般。所以我们选择只描绘变减速运动这一段,调整贝塞尔曲线为 cubic-bezier(.165, .84, .44, 1) 。

一次惯性滑动可能会出现两种情况:

惯性滑动的启动需要有足够的动量。我们可以简单地认为,当用户滑动的距离足够大(大于 15px )和持续时间足够短(小于 300ms )时,即可产生惯性滑动。也就是说,最后一次 touchmove 事件触发的时间和 touchend 事件触发的时间间隔小于 300ms ,且两者产生的距离差大于 15px 时认为启动惯性滑动。

当惯性滑动未结束(包括处于回弹过程),用户再次触碰滑块时会暂停滑块的运动。原理上是通过 getComputedStyle 和 getPropertyValue 方法获取当前的 transform: matrix() 矩阵值,抽离出水平 y 轴偏移量后重新调整 translate 的位置。

demo 基于 vuejs 实现,预览地址: https://codepen.io/JunreyCen/pen/arRYem

Android 自定义ScrollView 支持惯性滑动,惯性回弹效果。支持上拉加载更多

先讲下原理:

ScrollView的子View 主要分为3部分:head头部,滚动内容,fooder底部

我们实现惯性滑动,以及回弹,都是靠超过head或者fooder 就重新滚动到  ,内容的顶部或者底部。

之前看了Pulltorefresh 他是通过不断改变 head或者 fooder的 pading 值来实现 上拉或者 下拉的效果。感觉有点不流畅,而且层次嵌套得比较多。当然他的好处是扩展性好。

因工作需求,需要层次嵌套少,对性能要求非常高。因此重新自定义了ViewGroup实现。

技术分享

直接上代码:

  1. package com.example.administrator.customscrollview;  
  2.   
  3. import android.content.Context;  
  4. import android.content.res.TypedArray;  
  5. import android.util.AttributeSet;  
  6. import android.util.Log;  
  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.OverScroller;  
  14.   
  15. /** 
  16.  * 自定义 pulltorefresh Layout 
  17.  * TODO: ferris 2015年9月11日 18:52:40 
  18.  */  
  19. public class PullTorefreshScrollView extends ViewGroup {  
  20.   
  21.     private FoodeLayout fooder_layout;// top and buttom  
  22.     private View top_layout;  
  23.   
  24.     private int desireWidth, desireHeight;  
  25.     private VelocityTracker velocityTracker;  
  26.     private int mPointerId;  
  27.     private float x, y;  
  28.     private OverScroller mScroller;  
  29.     private int maxFlingVelocity, minFlingVelocity;  
  30.     private int mTouchSlop;  
  31.     protected Boolean isMove = false;  
  32.     protected float downX = 0, downY = 0;  
  33.     private int top_hight = 0;  
  34.     private int scrollYButtom = 0;  
  35.     private int nScrollYButtom = 0;  
  36.   
  37.     private int pullDownMin = 0;  
  38.     private Boolean isEnablePullDown = true;  
  39.   
  40.     private Boolean isFirst=true;  
  41.   
  42.     public void setEnablePullDown(Boolean isEnablePullDown) {  
  43.         this.isEnablePullDown = isEnablePullDown;  
  44.     }  
  45.   
  46.     public PullTorefreshScrollView(Context context) {  
  47.         super(context);  
  48.         init(null, 0);  
  49.     }  
  50.   
  51.     public PullTorefreshScrollView(Context context, AttributeSet attrs) {  
  52.         super(context, attrs);  
  53.         init(attrs, 0);  
  54.     }  
  55.   
  56.     public PullTorefreshScrollView(Context context, AttributeSet attrs, int defStyle) {  
  57.         super(context, attrs, defStyle);  
  58.         init(attrs, defStyle);  
  59.     }  
  60.   
  61.     private void init(AttributeSet attrs, int defStyle) {  
  62.         // Load attributes  
  63. //        final TypedArray a = getContext().obtainStyledAttributes(  
  64. //                attrs, R.styleable.PullTorefreshScrollView, defStyle, 0);  
  65. //  
  66. //  
  67. //        a.recycle();  
  68.         mScroller = new OverScroller(getContext());  
  69.         maxFlingVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity();  
  70.         minFlingVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();  
  71.         mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();  
  72.     }  
  73.   
  74.     @Override  
  75.     protected void onFinishInflate() {  
  76.         super.onFinishInflate();  
  77.         fooder_layout = (FoodeLayout) findViewById(R.id.fooder_layout);  
  78.         top_layout = findViewById(R.id.top_layout);  
  79.   
  80.   
  81.         if (isEnablePullDown) {  
  82.             fooder_layout.showFooderPull();  
  83.         } else {  
  84.             fooder_layout.hideFooder();  
  85.         }  
  86.     }  
  87.   
  88.   
  89.     public int getScrollYTop() {  
  90.         return top_hight;  
  91.     }  
  92.   
  93.     public int getScrollYButtom() {  
  94.         return scrollYButtom;  
  95.     }  
  96.   
  97.     public int getNScrollYTop() {  
  98.         return 0;  
  99.     }  
  100.   
  101.     public int getNScrollYButtom() {  
  102.         return nScrollYButtom;  
  103.     }  
  104.   
  105.     public int measureWidth(int widthMeasureSpec) {  
  106.         int result = 0;  
  107.         int measureMode = MeasureSpec.getMode(widthMeasureSpec);  
  108.         int width = MeasureSpec.getSize(widthMeasureSpec);  
  109.         switch (measureMode) {  
  110.             case MeasureSpec.AT_MOST:  
  111.             case MeasureSpec.EXACTLY:  
  112.                 result = width;  
  113.                 break;  
  114.             default:  
  115.                 break;  
  116.         }  
  117.         return result;  
  118.     }  
  119.   
  120.     public int measureHeight(int heightMeasureSpec) {  
  121.         int result = 0;  
  122.         int measureMode = MeasureSpec.getMode(heightMeasureSpec);  
  123.         int height = MeasureSpec.getSize(heightMeasureSpec);  
  124.         switch (measureMode) {  
  125.             case MeasureSpec.AT_MOST:  
  126.             case MeasureSpec.EXACTLY:  
  127.                 result = height;  
  128.                 break;  
  129.             default:  
  130.                 break;  
  131.         }  
  132.         return result;  
  133.     }  
  134.   
  135.     @Override  
  136.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  137.         // 计算所有child view 要占用的空间  
  138.         int width = measureWidth(widthMeasureSpec);  
  139.         int height = measureHeight(heightMeasureSpec);  
  140.   
  141.         desireWidth = 0;  
  142.         desireHeight = 0;  
  143.         int count = getChildCount();  
  144.         for (int i = 0; i < count; ++i) {  
  145.             View v = getChildAt(i);  
  146.   
  147.             if (v.getVisibility() != View.GONE) {  
  148.   
  149.                 LayoutParams lp = (LayoutParams) v.getLayoutParams();  
  150.                 measureChildWithMargins(v, widthMeasureSpec, 0,  
  151.                         heightMeasureSpec, 0);  
  152.   
  153.                 //只是在这里增加了垂直或者水平方向的判断  
  154.                 if (v.getId() == R.id.top_layout) {  
  155.                     top_hight = v.getMeasuredHeight();  
  156.                 }  
  157.                 desireWidth = Math.max(desireWidth, v.getMeasuredWidth()  
  158.                         + lp.leftMargin + lp.rightMargin);  
  159.                 desireHeight += v.getMeasuredHeight() + lp.topMargin  
  160.                         + lp.bottomMargin;  
  161.   
  162.             }  
  163.         }  
  164.   
  165.         // count with padding  
  166.         desireWidth += getPaddingLeft() + getPaddingRight();  
  167.         desireHeight += getPaddingTop() + getPaddingBottom();  
  168.   
  169.         // see if the size is big enough  
  170.         desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());  
  171.         desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());  
  172.   
  173.   
  174.         //处理内容比较少的时候,就添加一定的高度  
  175.         int scrollHight = height + top_hight * 2;  
  176.         if (scrollHight > desireWidth) {  
  177.             int offset = scrollHight - desireHeight;  
  178.             View view = new View(getContext());  
  179.             view.setBackgroundResource(R.color.top_layout_color);  
  180.             LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, offset);  
  181.             addView(view, getChildCount() - 1, lp);  
  182.             desireWidth = scrollHight;  
  183.         }  
  184.   
  185.   
  186.         setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),  
  187.                 resolveSize(desireHeight, heightMeasureSpec));  
  188.   
  189.         scrollYButtom = desireHeight - getMeasuredHeight() - top_hight;  
  190.         nScrollYButtom = desireHeight - getMeasuredHeight();  
  191.         //如果上啦拖出一半的高度,就代表将要执行上啦  
  192.         pullDownMin = nScrollYButtom - top_hight / 2;  
  193.         if(isFirst){  
  194.             scrollTo(0, top_hight);  
  195.             isFirst=false;  
  196.         }  
  197.   
  198.     }  
  199.   
  200.     @Override  
  201.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  202.         final int parentLeft = getPaddingLeft();  
  203.         final int parentRight = r - l - getPaddingRight();  
  204.         final int parentTop = getPaddingTop();  
  205.         final int parentBottom = b - t - getPaddingBottom();  
  206.   
  207.         if (BuildConfig.DEBUG)  
  208.             Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "  
  209.                     + parentTop + "   parentright: " + parentRight  
  210.                     + "   parentbottom: " + parentBottom);  
  211.   
  212.         int left = parentLeft;  
  213.         int top = parentTop;  
  214.   
  215.         int count = getChildCount();  
  216.         for (int i = 0; i < count; ++i) {  
  217.             View v = getChildAt(i);  
  218.             if (v.getVisibility() != View.GONE) {  
  219.                 LayoutParams lp = (LayoutParams) v.getLayoutParams();  
  220.                 final int childWidth = v.getMeasuredWidth();  
  221.                 final int childHeight = v.getMeasuredHeight();  
  222.                 final int gravity = lp.gravity;  
  223.                 final int horizontalGravity = gravity  
  224.                         & Gravity.HORIZONTAL_GRAVITY_MASK;  
  225.                 final int verticalGravity = gravity  
  226.                         & Gravity.VERTICAL_GRAVITY_MASK;  
  227.   
  228.   
  229.                 // layout vertical, and only consider horizontal gravity  
  230.   
  231.                 left = parentLeft;  
  232.                 top += lp.topMargin;  
  233.                 switch (horizontalGravity) {  
  234.                     case Gravity.LEFT:  
  235.                         break;  
  236.                     case Gravity.CENTER_HORIZONTAL:  
  237.                         left = parentLeft  
  238.                                 + (parentRight - parentLeft - childWidth) / 2  
  239.                                 + lp.leftMargin - lp.rightMargin;  
  240.                         break;  
  241.                     case Gravity.RIGHT:  
  242.                         left = parentRight - childWidth - lp.rightMargin;  
  243.                         break;  
  244.                 }  
  245.                 v.layout(left, top, left + childWidth, top + childHeight);  
  246.                 top += childHeight + lp.bottomMargin;  
  247.             }  
  248.   
  249.         }  
  250.   
  251.   
  252.     }  
  253.   
  254.     @Override  
  255.     protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {  
  256.         return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
  257.                 ViewGroup.LayoutParams.MATCH_PARENT);  
  258.     }  
  259.   
  260.     @Override  
  261.     public android.view.ViewGroup.LayoutParams generateLayoutParams(  
  262.             AttributeSet attrs) {  
  263.         return new LayoutParams(getContext(), attrs);  
  264.     }  
  265.   
  266.     @Override  
  267.     protected android.view.ViewGroup.LayoutParams generateLayoutParams(  
  268.             android.view.ViewGroup.LayoutParams p) {  
  269.         return new LayoutParams(p);  
  270.     }  
  271.   
  272.     public static class LayoutParams extends MarginLayoutParams {  
  273.         public int gravity = -1;  
  274.   
  275.         public LayoutParams(Context c, AttributeSet attrs) {  
  276.             super(c, attrs);  
  277.   
  278.             TypedArray ta = c.obtainStyledAttributes(attrs,  
  279.                     R.styleable.SlideGroup);  
  280.   
  281.             gravity = ta.getInt(R.styleable.SlideGroup_layout_gravity, -1);  
  282.   
  283.             ta.recycle();  
  284.         }  
  285.   
  286.         public LayoutParams(int width, int height) {  
  287.             this(width, height, -1);  
  288.         }  
  289.   
  290.         public LayoutParams(int width, int height, int gravity) {  
  291.             super(width, height);  
  292.             this.gravity = gravity;  
  293.         }  
  294.   
  295.         public LayoutParams(android.view.ViewGroup.LayoutParams source) {  
  296.             super(source);  
  297.         }  
  298.   
  299.         public LayoutParams(MarginLayoutParams source) {  
  300.             super(source);  
  301.         }  
  302.     }  
  303.   
  304.   
  305.     /** 
  306.      * onInterceptTouchEvent()用来询问是否要拦截处理。 onTouchEvent()是用来进行处理。 
  307.      * <p/> 
  308.      * 例如:parentLayout----childLayout----childView 事件的分发流程: 
  309.      * parentLayout::onInterceptTouchEvent()---false?---> 
  310.      * childLayout::onInterceptTouchEvent()---false?---> 
  311.      * childView::onTouchEvent()---false?---> 
  312.      * childLayout::onTouchEvent()---false?---> parentLayout::onTouchEvent() 
  313.      * <p/> 
  314.      * <p/> 
  315.      * <p/> 
  316.      * 如果onInterceptTouchEvent()返回false,且分发的子View的onTouchEvent()中返回true, 
  317.      * 那么onInterceptTouchEvent()将收到所有的后续事件。 
  318.      * <p/> 
  319.      * 如果onInterceptTouchEvent()返回true,原本的target将收到ACTION_CANCEL,该事件 
  320.      * 将会发送给我们自己的onTouchEvent()。 
  321.      */  
  322.     @Override  
  323.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  324.         final int action = ev.getActionMasked();  
  325.         if (BuildConfig.DEBUG)  
  326.             Log.d("onInterceptTouchEvent", "action: " + action);  
  327.   
  328.         if (action == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {  
  329.             // 该事件可能不是我们的  
  330.             return false;  
  331.         }  
  332.   
  333.         boolean isIntercept = false;  
  334.         switch (action) {  
  335.             case MotionEvent.ACTION_DOWN:  
  336.                 // 如果动画还未结束,则将此事件交给onTouchEvet()处理,  
  337.                 // 否则,先分发给子View  
  338.                 isIntercept = !mScroller.isFinished();  
  339.                 // 如果此时不拦截ACTION_DOWN时间,应该记录下触摸地址及手指id,当我们决定拦截ACTION_MOVE的event时,  
  340.                 // 将会需要这些初始信息(因为我们的onTouchEvent将可能接收不到ACTION_DOWN事件)  
  341.                 mPointerId = ev.getPointerId(0);  
  342. //          if (!isIntercept) {  
  343.                 downX = x = ev.getX();  
  344.                 downY = y = ev.getY();  
  345. //          }  
  346.                 break;  
  347.             case MotionEvent.ACTION_MOVE:  
  348.                 int pointerIndex = ev.findPointerIndex(mPointerId);  
  349.                 if (BuildConfig.DEBUG)  
  350.                     Log.d("onInterceptTouchEvent", "pointerIndex: " + pointerIndex  
  351.                             + ", pointerId: " + mPointerId);  
  352.                 float mx = ev.getX(pointerIndex);  
  353.                 float my = ev.getY(pointerIndex);  
  354.   
  355.                 if (BuildConfig.DEBUG)  
  356.                     Log.d("onInterceptTouchEvent", "action_move [touchSlop: "  
  357.                             + mTouchSlop + ", deltaX: " + (x - mx) + ", deltaY: "  
  358.                             + (y - my) + "]");  
  359.   
  360.                 // 根据方向进行拦截,(其实这样,如果我们的方向是水平的,里面有一个ScrollView,那么我们是支持嵌套的)  
  361.   
  362.                 if (Math.abs(y - my) >= mTouchSlop) {  
  363.                     isIntercept = true;  
  364.                 }  
  365.   
  366.   
  367.                 //如果不拦截的话,我们不会更新位置,这样可以通过累积小的移动距离来判断是否达到可以认为是Move的阈值。  
  368.                 //这里当产生拦截的话,会更新位置(这样相当于损失了mTouchSlop的移动距离,如果不更新,可能会有一点点跳的感觉)  
  369.                 if (isIntercept) {  
  370.                     x = mx;  
  371.                     y = my;  
  372.                 }  
  373.                 break;  
  374.             case MotionEvent.ACTION_CANCEL:  
  375.             case MotionEvent.ACTION_UP:  
  376.                 // 这是触摸的最后一个事件,无论如何都不会拦截  
  377.                 if (velocityTracker != null) {  
  378.                     velocityTracker.recycle();  
  379.                     velocityTracker = null;  
  380.                 }  
  381.                 break;  
  382.             case MotionEvent.ACTION_POINTER_UP:  
  383.                 solvePointerUp(ev);  
  384.   
  385.                 break;  
  386.         }  
  387.         return isIntercept;  
  388.     }  
  389.   
  390.     private void solvePointerUp(MotionEvent event) {  
  391.         // 获取离开屏幕的手指的索引  
  392.         int pointerIndexLeave = event.getActionIndex();  
  393.         int pointerIdLeave = event.getPointerId(pointerIndexLeave);  
  394.         if (mPointerId == pointerIdLeave) {  
  395.             // 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTracker  
  396.             int reIndex = pointerIndexLeave == 0 ? 1 : 0;  
  397.             mPointerId = event.getPointerId(reIndex);  
  398.             // 调整触摸位置,防止出现跳动  
  399.             x = event.getX(reIndex);  
  400.             y = event.getY(reIndex);  
  401.             if (velocityTracker != null)  
  402.                 velocityTracker.clear();  
  403.         }  
  404.     }  
  405.   
  406.     @Override  
  407.     public boolean onTouchEvent(MotionEvent event) {  
  408.   
  409.         final int action = event.getActionMasked();  
  410.   
  411.         if (velocityTracker == null) {  
  412.             velocityTracker = VelocityTracker.obtain();  
  413.         }  
  414.         velocityTracker.addMovement(event);  
  415.   
  416.         switch (action) {  
  417.             case MotionEvent.ACTION_DOWN:  
  418.                 // 获取索引为0的手指id  
  419.   
  420.   
  421.                 isMove = false;  
  422.                 mPointerId = event.getPointerId(0);  
  423.                 x = event.getX();  
  424.                 y = event.getY();  
  425.                 if (!mScroller.isFinished())  
  426.                     mScroller.abortAnimation();  
  427.                 break;  
  428.             case MotionEvent.ACTION_MOVE:  
  429.                 isMove = true;  
  430.                 // 获取当前手指id所对应的索引,虽然在ACTION_DOWN的时候,我们默认选取索引为0  
  431.                 // 的手指,但当有第二个手指触摸,并且先前有效的手指up之后,我们会调整有效手指  
  432.   
  433.                 // 屏幕上可能有多个手指,我们需要保证使用的是同一个手指的移动轨迹,  
  434.                 // 因此此处不能使用event.getActionIndex()来获得索引  
  435.                 final int pointerIndex = event.findPointerIndex(mPointerId);  
  436.                 float mx = event.getX(pointerIndex);  
  437.                 float my = event.getY(pointerIndex);  
  438.   
  439.                 moveBy((int) (x - mx), (int) (y - my));  
  440.   
  441.                 x = mx;  
  442.                 y = my;  
  443.                 break;  
  444.             case MotionEvent.ACTION_UP:  
  445.                 isMove = false;  
  446.                 velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);  
  447.                 float velocityX = velocityTracker.getXVelocity(mPointerId);  
  448.                 float velocityY = velocityTracker.getYVelocity(mPointerId);  
  449.   
  450.                 completeMove(-velocityX, -velocityY);  
  451.                 if (velocityTracker != null) {  
  452.                     velocityTracker.recycle();  
  453.                     velocityTracker = null;  
  454.                 }  
  455.                 break;  
  456.   
  457.             case MotionEvent.ACTION_POINTER_UP:  
  458.                 // 获取离开屏幕的手指的索引  
  459.                 isMove = false;  
  460.                 int pointerIndexLeave = event.getActionIndex();  
  461.                 int pointerIdLeave = event.getPointerId(pointerIndexLeave);  
  462.                 if (mPointerId == pointerIdLeave) {  
  463.                     // 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTracker  
  464.                     int reIndex = pointerIndexLeave == 0 ? 1 : 0;  
  465.                     mPointerId = event.getPointerId(reIndex);  
  466.                     // 调整触摸位置,防止出现跳动  
  467.                     x = event.getX(reIndex);  
  468.                     y = event.getY(reIndex);  
  469.                     if (velocityTracker != null)  
  470.                         velocityTracker.clear();  
  471.                 }  
  472.                 break;  
  473.             case MotionEvent.ACTION_CANCEL:  
  474.                 isMove = false;  
  475.                 break;  
  476.         }  
  477.         return true;  
  478.     }  
  479.   
  480.     private Boolean isPull = false;  
  481.   
  482.     //此处的moveBy是根据水平或是垂直排放的方向,  
  483. //来选择是水平移动还是垂直移动  
  484.     public void moveBy(int deltaX, int deltaY) {  
  485.         if (BuildConfig.DEBUG)  
  486.             Log.d("moveBy", "deltaX: " + deltaX + "    deltaY: " + deltaY);  
  487.         if (Math.abs(deltaY) >= Math.abs(deltaX)) {  
  488.             int mScrollY = getScrollY();  
  489.             if (mScrollY <= 0) {  
  490.                 scrollTo(0, 0);  
  491.             } else if (mScrollY >= getNScrollYButtom()) {  
  492.                 scrollTo(0, getNScrollYButtom());  
  493.   
  494.   
  495.             } else {  
  496.                 scrollBy(0, deltaY);  
  497.             }  
  498.   
  499.             if (isEnablePullDown && deltaY > 0 && mScrollY >= pullDownMin) {  
  500.                 isPull = true;  
  501.                 Log.d("onlayout", "isPull: true");  
  502.             }  
  503.         }  
  504.   
  505.   
  506.     }  
  507.   
  508.     private void completeMove(float velocityX, float velocityY) {  
  509.   
  510.         int mScrollY = getScrollY();  
  511.         int maxY = getScrollYButtom();  
  512.         int minY = getScrollYTop();  
  513.   
  514.   
  515.         if (mScrollY >= maxY) {//如果滚动,超过了 下边界,就回弹到下边界  
  516.   
  517.             if (isPull) {//滚动到最底部  
  518.                 mScroller.startScroll(0, mScrollY, 0, getNScrollYButtom() - mScrollY, 300);  
  519.                 invalidate();  
  520.   
  521.                 //显示雷达  
  522.                 fooder_layout.showFooderRadar();  
  523.                 if (pullDownListem != null) {  
  524.                     pullDownListem.onPullDown();  
  525.                 }  
  526.                 Log.d("onlayout", "isPull: true 滚动到最底部,显示出雷达");  
  527.   
  528.             } else {  
  529.                 mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY);  
  530.                 invalidate();  
  531.                 Log.d("onlayout", "isPull: true");  
  532.             }  
  533.   
  534.         } else if (mScrollY <= minY) {//如果滚动,超过了上边界,就回弹到上边界  
  535.             // 超出了上边界,弹回  
  536.             mScroller.startScroll(0, mScrollY, 0, minY - mScrollY);  
  537.             invalidate();  
  538.         } else if (Math.abs(velocityY) >= minFlingVelocity && maxY > 0) {//大于1页的时候  
  539. //            mScroller.fling(0, mScrollY, 0, (int) (velocityY * 1.2f), 0, 0, minY, maxY);  
  540.             mScroller.fling(0, mScrollY, 0, (int) (velocityY * 2f), 0, 0, getNScrollYTop(), getNScrollYButtom());  
  541.             invalidate();  
  542.         }  
  543.   
  544.   
  545.     }  
  546.   
  547.   
  548.     public void ifNeedScrollBack() {  
  549.         int mScrollY = getScrollY();  
  550.         int maxY = getScrollYButtom();  
  551.         int minY = getScrollYTop();  
  552.         if (mScrollY > maxY) {  
  553.             // 超出了下边界,弹回  
  554.             mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY);  
  555.   
  556.             invalidate();  
  557.   
  558.         } else if (mScrollY < minY) {  
  559.             // 超出了上边界,弹回  
  560.             mScroller.startScroll(0, mScrollY, 0, minY - mScrollY);  
  561.             invalidate();  
  562.         }  
  563.     }  
  564.   
  565.     @Override  
  566.     protected void onScrollChanged(int l, int t, int oldl, int oldt) {  
  567.         super.onScrollChanged(l, t, oldl, oldt);  
  568.     }  
  569.   
  570.     @Override  
  571.     public void computeScroll() {  
  572.         if (mScroller.computeScrollOffset()) {  
  573.   
  574.             scrollTo(0, mScroller.getCurrY());  
  575.   
  576.             postInvalidate();  
  577.   
  578.         } else {  
  579.             Log.d("onlayout", "computeScroll,isMove:"+isMove+",isPull:"+isPull);  
  580.             if (!isMove && !isPull) {  
  581.                 ifNeedScrollBack();  
  582.             }  
  583.   
  584.         }  
  585.     }  
  586.   
  587.   
  588.     public void onPullSuccess() {  
  589.   
  590.         soomToBack();  
  591.     }  
  592.   
  593.     public void soomToBack() {  
  594.         int mScrollY = getScrollY();  
  595.         int maxY = getScrollYButtom();  
  596.         Log.d("onlayout", "soomToBack: (maxY - mScrollY)="+(maxY - mScrollY)+",maxY="+maxY+",mScrollY="+mScrollY);  
  597.         // 超出了下边界,弹回  
  598.         mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY, 300);  
  599.         invalidate();  
  600.         postDelayed(new Runnable() {  
  601.             @Override  
  602.             public void run() {  
  603.                 fooder_layout.showFooderPull();  
  604.                 isPull = false;  
  605.             }  
  606.         }, 310);  
  607.   
  608.   
  609.     }  
  610.   
  611.     private PullDownListem pullDownListem;  
  612.   
  613.     public void setPullDownListem(PullDownListem pullDownListem) {  
  614.         this.pullDownListem = pullDownListem;  
  615.     }  
  616.   
  617.     public interface PullDownListem {  
  618.   
  619.         public void onPullDown();  
  620.   
  621.     }  

以上是关于物理学的 H5 应用:模拟惯性滑动的主要内容,如果未能解决你的问题,请参考以下文章

仅在物理设备而非模拟器上运行 Android 应用程序

TCP通过滑动窗口和拥塞窗口实现限流,能抵御ddos攻击吗

H5监听Android物理返回键

自行车平衡原理

物理应用基于matlab模拟井筒多相流含Matlab源码 2152期

H5页面在 ios 端滑动不流畅的问题