前面只实现了内容随手指滑动而滑动,如何在手指离开屏幕以后还能继续平滑滑动一段距离呢?

要实现平滑滑动,需要借助于Scroller类,它其实是通过起始位置、终止位置或初始速度,加上持续时间,计算出中间时刻的滑动位置,然后不断触发重绘来实现的。即通过不断的移动一个小的距离来实现整体的平滑滑动效果。

提供了两个方法

startScroll(),给它传入滑动的起始位置、终止位置和持续时间。

ios开发 oc UISlider 手指离开屏幕时_Math


fling(),给他传入手初始速度和起始位置、终止位置的范围。(不过在我这里的实验结果,在靠近边界的时候,fling()效果不是很好)

 

ios开发 oc UISlider 手指离开屏幕时_ci_02


具体使用方式如下

1.     在ACTION_UP中根据速度分别调用startScroll()或者fling(),并调用invalidate()触发重绘。

2.     重写computeScroll()方法(),在其中通过Scroller获取到中间时刻的滑动位置,并调用scrollTo()/scrollBy()方法(会设置新的滑动位置并触发重绘),并再次调用invalidate()。

3.     可以在ACTION_DOWN中进行处理,如果正在惯性滑动,则在手指按下时取消当前的惯性滑动。

public class MyFrameLayout extends FrameLayout {

    //内容的最大值、最小值
    private int minX=0;
    private int maxX=0;

    //滑动坐标的最大值、最小值
    private int minScrollX;
    private int maxScrollX;

    private int lastX;

    VelocityTracker mVelocityTracker;
    int scaledMaximumFlingVelocity;
    int scaledMinimumFlingVelocity;

    private MyScroller mScroller;

    public MyFrameLayout(Context context) {
       super(context);
       init();
    }

    public MyFrameLayout(Context context, AttributeSet attrs) {
       super(context, attrs);
       init();
    }

    public MyFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
        init();
    }

    public void init(){
       ViewConfiguration configuration = ViewConfiguration.get(getContext());
       scaledMinimumFlingVelocity=configuration.getScaledMinimumFlingVelocity();
       scaledMaximumFlingVelocity= configuration.getScaledMaximumFlingVelocity();
       Log.e("shadowfaxghh", "scaledMinimumFlingVelocity="+scaledMinimumFlingVelocity+" scaledMaximumFlingVelocity="+scaledMaximumFlingVelocity);

       mScroller=new MyScroller(getContext());
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
       super.onLayout(changed, left, top, right, bottom);

       //计算出内容的最大值最小值
       for(int i=0; i<getChildCount(); i++){
           View childView = getChildAt(i);

           if(minX>childView.getLeft())
               minX=childView.getLeft();

           if(maxX<childView.getRight())
               maxX=childView.getRight();
       }

       //计算出滑动坐标的最大值、最小值//只有内容大小超出自身大小时,才能进行滑动
       if(minX<0)
           minScrollX=minX;
       else
           minScrollX=0;

       if(maxX>getWidth())
           maxScrollX=maxX-getWidth();
       else
           maxScrollX=0;
    }

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

       switch (event.getAction()){
           case MotionEvent.ACTION_DOWN:
//               Log.e("shadowfaxghh", "ACTION_DOWN");
               lastX=(int) event.getRawX();

               obtaionVelocityTracker();
               mVelocityTracker.addMovement(event);

               if(mScroller!=null & !mScroller.isFinished()){
                   mScroller.abortAnimation();
               }
               break;

           case MotionEvent.ACTION_MOVE:
//               Log.e("shadowfaxghh", "ACTION_MOVE");
               int newX= (int) event.getRawX();
               int deltaX=newX-lastX;

//               if (Math.abs(deltaX) >ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
               int newScrollX = getScrollX() + (-deltaX);
               if (newScrollX > maxScrollX)
                   newScrollX = maxScrollX;
               if (newScrollX < minScrollX)
                   newScrollX = minScrollX;

               scrollTo(newScrollX, getScrollY());

               //更新坐标
               lastX = newX;
//                }

               obtaionVelocityTracker();
               mVelocityTracker.addMovement(event);
               break;

           case MotionEvent.ACTION_UP:
               Log.e("shadowfaxghh", "ACTION_UP");
               lastX=(int) event.getRawX();

               if(mVelocityTracker!=null) {
                   //计算速度
                   mVelocityTracker.computeCurrentVelocity(1000, scaledMaximumFlingVelocity);

                   //获取x方向的速度
                   int xVelocity = (int) mVelocityTracker.getXVelocity();
                   Log.e("shadowfaxghh", "xVelocity=" + xVelocity);

                   recycleVelocityTracker();

                   //使用startScroll()进行滑动
                   Log.e("shadowfaxghh", "startScroll()");

                   //假定手指离开以后以当前速度/4继续滑动1000ms
                   int scrollDuration=1000;
                   int scrollDistance=Math.abs(scrollDuration/1000 * xVelocity/4);//computeCurrentVelocity(1000//速度是以1000ms为单位的

                   int startScrollX = getScrollX();
                   int endScrollX =startScrollX;
                   if(xVelocity>0){// 手指向右滑动
                       endScrollX=startScrollX-scrollDistance;
                   }else if(xVelocity<0){// 手指向左滑动
                       endScrollX=startScrollX+scrollDistance;
                   }

                   if(endScrollX<minScrollX)
                       endScrollX=minScrollX;
                   if(endScrollX>maxScrollX)
                       endScrollX=maxScrollX;

                   //实际滑动时间ms//当然这是以匀速滑动来计算的,实际上Scroller内部并不是匀速滑动
                   scrollDuration= (int) (Math.abs((float) (startScrollX-endScrollX)/(float)scrollDistance)*scrollDuration);
                   mScroller.startScroll(startScrollX, getScrollY(), endScrollX-startScrollX, 0, scrollDuration);
                   invalidate();

//                   //使用Fling()进行滑动
 //                   if(Math.abs(xVelocity)>scaledMinimumFlingVelocity) {//快速滑动
 //                       Log.e("shadowfaxghh", "fling()");
 //
 //                       //注意xVelocity要取反,因为velocity正负值跟屏幕坐标系一致,但是scroll参数跟屏幕坐标系相反
 //                       mScroller.fling(getScrollX(), getScrollY(), -xVelocity, 0, minScrollX,maxScrollX, 0, 0);
 //                       invalidate();
 //                   }
               }
               break;

           case MotionEvent.ACTION_CANCEL:
               Log.e("shadowfaxghh", "ACTION_CANCEL");
               lastX=(int) event.getRawX();
               recycleVelocityTracker();
               break;

           default:
               break;
       }
       return true;
    }

    @Override
    public void computeScroll() {
//       super.computeScroll();
       if(mScroller!=null && mScroller.computeScrollOffset()){
           int currX = mScroller.getCurrX();
           int currY = mScroller.getCurrY();
           scrollTo(currX, currY);
           invalidate();//某些情况下scrollTo()无法触发重绘,需要自己invalidate()触发
       }
    }

    private void obtaionVelocityTracker(){
       if(mVelocityTracker==null)
           mVelocityTracker=VelocityTracker.obtain();
    }

    private void recycleVelocityTracker(){
       if(mVelocityTracker!=null){
           mVelocityTracker.clear();
           mVelocityTracker.recycle();
           mVelocityTracker=null;
       }
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
       super.onScrollChanged(l, t, oldl, oldt);
    }
 }

可以看一下Scroller中相关方法的源码,如下代码基于android-2.3.3_r1

startScroll()
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;

    Log.e("shadowfaxghh", "Scroller startScroll()mStartX="+mStartX+" mFinalX="+mFinalX+" mDuration="+mDuration);
}

 

fling()
public void fling(int startX, int startY, int velocityX, int velocityY,
                 int minX, int maxX, int minY, int maxY) {
    mMode = FLING_MODE;
    mFinished = false;

    float velocity = (float)Math.hypot(velocityX, velocityY);

    mVelocity = velocity;
    //计算出滑动时间
    mDuration = (int) (1000 * velocity / mDeceleration); // Duration is in
    // milliseconds
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;

    mCoeffX = velocity == 0 ? 1.0f : velocityX / velocity;
    mCoeffY = velocity == 0 ? 1.0f : velocityY / velocity;

    //计算出滑动距离
    int totalDistance = (int) ((velocity * velocity) / (2 * mDeceleration));

    mMinX = minX;
    mMaxX = maxX;
    mMinY = minY;
    mMaxY = maxY;    //跟传入的参数进行比较
    mFinalX = startX + Math.round(totalDistance* mCoeffX);
    // Pin to mMinX <= mFinalX <=mMaxX
    mFinalX = Math.min(mFinalX, mMaxX);
    mFinalX = Math.max(mFinalX, mMinX);

    mFinalY = startY + Math.round(totalDistance* mCoeffY);
    // Pin to mMinY <= mFinalY <=mMaxY
    mFinalY = Math.min(mFinalY, mMaxY);
    mFinalY = Math.max(mFinalY, mMinY);
    Log.e("shadowfaxghh", "Scroller fling() mStartX="+mStartX+" mFinalX="+mFinalX+" mDuration="+mDuration);
}

 

computeScrollOffset()
public boolean computeScrollOffset() {//返回滑动是否已经结束
    if (mFinished) {
       return false;
    }

    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis()- mStartTime);
    //从开始滑动到现在还没有结束
    if (timePassed < mDuration) {
       switch (mMode) {//分两种模式计算中间时刻的速度
           case SCROLL_MODE:
               float x = (float)timePassed * mDurationReciprocal;

               if (mInterpolator == null)
                   x = viscousFluid(x);
               else
                   x = mInterpolator.getInterpolation(x);

               mCurrX = mStartX + Math.round(x * mDeltaX);
               mCurrY = mStartY + Math.round(x * mDeltaY);
               break;
           case FLING_MODE:
               float timePassedSeconds = timePassed / 1000.0f;
               float distance = (mVelocity * timePassedSeconds)
                       - (mDeceleration * timePassedSeconds * timePassedSeconds/ 2.0f);

                mCurrX = mStartX + Math.round(distance * mCoeffX);
               // Pin to mMinX<= mCurrX <= mMaxX
               mCurrX = Math.min(mCurrX, mMaxX);
               mCurrX = Math.max(mCurrX, mMinX);

               mCurrY = mStartY + Math.round(distance * mCoeffY);
               // Pin to mMinY<= mCurrY <= mMaxY
               mCurrY = Math.min(mCurrY, mMaxY);
               mCurrY = Math.max(mCurrY, mMinY);

               if (mCurrX == mFinalX && mCurrY == mFinalY) {
                   mFinished = true;
               }

               break;
       }
    }
    else {
       mCurrX = mFinalX;
       mCurrY = mFinalY;
       mFinished = true;
    }
    return true;
}

 

startScroll() vs fling()

参考ScrollView的源码,startScroll()和fling()并不是同时使用的

当手指离开屏幕的时候,如果速度大于minFlingVelocity,则进行fling()。否则停止(或回弹)。