四、界面编程(四) View的滑动及滑动冲突详解

来源:互联网 发布:数据库2008安装教程 编辑:程序博客网 时间:2024/05/29 15:28

  • View的滑动
    • 1 使用scrollToscrollBy
    • 2 使用动画
    • 3 改变布局参数
    • 4 各种滑动方式的比较
  • 弹性滑动
    • 1 使用Scroller
    • 2 通过动画
  • View的滑动冲突处理
    • 1常见的滑动冲突场景
      • 11 外部滑动方向和内部滑动的方向不一致
      • 12 外部滑动方向和内部滑动方向一致
    • 2 滑动冲突的解决
      • 21 外部拦截法
      • 22 内部拦截法

1.View的滑动

  View的滑动应用非常广泛,不论是下拉刷新还是SlidingMenu,他们的基础都是滑动。掌握滑动的方法是实现自定义控件的基础。目前,常用的滑动方式是以下三种:
(1)通过View本身提供的scrollTo/scrollBy方法来实现滑动;
(2)通过动画给View施加平移效果来实现滑动
(3)改变View的LayoutParams使得View重新布局从而实现滑动

1.1 使用scrollTo/scrollBy

  为了实现View的滑动,View提供了专门的方法来实现这个功能。那就是scrollTo/scrollBy。经过分析源码得知,scrollBy实际上也是调用了scrollTo方法,而scrollTo则实现了基于所传递参数的绝对滑动。利用scrollTo和scrollBy来实现View的滑动,这不困难,但我们要知道。滑动过程中,View内部的两个属性mScrollX和mScrollY的改变规则。这两个属性可以通过getScrollX和getScrollY方法分别得到。
  在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向上的距离,而mScrollY的值,总是等于View上边缘和View内容上边缘的竖直方向的距离。scrollTo和scrollBy只能改变View的内容的位置,而不能改变View在布局中的位置。mScrollX和mScrollY的单位是像素,并且当View左边缘在View内容左边缘右边时,mScrollX为正值,反之为负值。当View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。换句话说,如果从左向右滑动,那么mScrollX为负值,反之为正值。如果从上往下滑动,mScrollY为负值,反之为正值。

1.2 使用动画

  使用动画来移动View,主要操作View的translationX和translationY属性,即可以使用传统的View动画来完成,也可以采用属性动画,如果采用属性动画的话,为了兼容3.0一下版本,需要采用开源动画库nineoldandroids(http://nineoldandroids.com/)
  使用传统的View动画,可以xml中的指定translate,采用属性动画,更简单了,比如:
  ObjectAnimator.ofFloat(targetView,”translationX”,0,100).setDuration(100).start();

  需要注意的一点是,View动画是对View的影像的操作,它并不能真正改变View的位置包括宽/高,造成View无法响应点击事件,因为View的真身还在原来的位置,并且如果希望动画后的状态得以保留。还必须将fillAfter的属性设置为true,否则动画完成后,其动画效果就会消失。

1.3 改变布局参数

  改变布局参数,即改变LayoutParams比较好理解,比如我们想把一个Button向右平移100px,我们主要将这个Button的LayoutParams里的marginLeft参数的值增加100px即可。

MarginLayoutParams params = (MarginLayoutParams )mButton1.getLayoutParams();Params.width +=100;params.leftMargin +=100;mButton1.requestLayout();

  有一种情形,为了达到移动Button的目的,我们可以在Button的左边放置一个空的view,这个View的默认宽度为0,当我们需要向右移动Button时,只需要重新设置空View的宽度即可,当空View的宽度增大时,Button就会被挤到右边,即实现了向右平移的效果。

1.4 各种滑动方式的比较

(1)scrollTo/scrollBy方式:它是View提供的原生方法,作用是专门用于VIew的滑动,它可以比较方便地实现滑动效果并且不影响内部元素的单击事件。但是它的缺点也是很显然的,它只能滑动VIew的内容,并不能滑动View本身。即操作简单,适合View内容的滑动。
(2)动画:实际使用中,如果动画元素不需要响应用户的交互,那么使用动画来做滑动比较合适,否则就不太合适。但是动画有一个很明显的优点,那就是一些复杂的动画效果必须通过动画才能实现。即操作简单,主要适用于没有交互的View和实现复杂的动画效果。
(3)改变布局的方式:操作稍微复杂,适用于有交互的View

2.弹性滑动

  View生硬的滑动过去,用户体验实在太差,因此,我们要实现渐进式的滑动,渐进式滑动有一个共同思想,将依次大的滑动分成若干次小的滑动,并在一个时间段内完成。常用的弹性滑动有三种方式:
(1)使用scroller
(2)使用动画
(3)延时策略

2.1 使用Scroller

Scroller的使用方法

Scroller mScroller = new Scroller(mContext);    //缓慢滑动到指定位置    private void smoothScrollTo(int destX,int destY){        int scrollX = getScrollX();        int deltaX = destX - scrollX;        //1000ms内滑向destX,效果就是慢慢滑动        mScroller.startScroll(scrollX,0,deltaX,0,1000);        invalidate();    }    public void computeScroll(){        if(mScroller.computeScrollOffset()){            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());            postInvalidate();        }    }

以上是Scroller的典型使用方法,它的工作原理如下:
  当我们构造了一个Scroller对象并且调用它的startScroller方法时,Scroller内部其实什么也没做,它只是保存了我们传递的几个参数,这几个参数从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;  //滑动的起点x坐标        mStartY = startY;  //滑动的起点y坐标        mFinalX = startX + dx;  //滑动的终点x坐标        mFinalY = startY + dy;  //滑动的终点y坐标        mDeltaX = dx; //x的滑动距离        mDeltaY = dy; //y的滑动距离        mDurationReciprocal = 1.0f / (float) mDuration;    }

  可以从代码中看出,仅仅调用startScroll方法是无法让View滑动的,因为它内部并没有做滑动相关的事,那么Scroller到底是如何让View弹性滑动的呢?答案是startScroll方法下面的invalidate方法。invalidate方法会导致View的重绘,在View的draw方法中又会去调用computeScroll方法,computeScroll方法在View中是一个空实现,因此需要我们自己去实现,上面的代码实现了computeScroll方法。具体过程如下:当View重绘后再draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致computeScroll方法被调用;然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到新的位置,如此反复,直到整个滑动过程结束。

我们再来看一下Scroller的computeScrollOffset方法的实现,源码如下:

public boolean computeScrollOffset() {        if (mFinished) {            return false;        }        int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);        if (timePassed < mDuration) {            switch (mMode) {                case SCROLL_MODE:                    final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);                    mCurrX = mStartX + Math.round(x * mDeltaX);                    mCurrY = mStartY + Math.round(x * mDeltaY);                    break;             ....            }            ...            return true;        }    }

  这个方法会根据时间流逝来计算当前的scrollX和scrollY的值。计算方法也很简单,大意就是根据时间流逝的百分比来计算出scrollX和scrollY改变的百分比并计算出当前的值,这个过程类似与动画中的插值器的概念,这里我们先不去深入探究这个过程。这个方法返回true表示滑动还未结束,返回false表示滑动已经结束,因此当这个方法返回true的时候我们要继续进行View的滑动。

2.2 通过动画

  动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果,比如以下代码可以让一个View的内容再100ms内向左移动100像素。

ObjectAnimation.ofFloat(targetView,"translationX",0,100).setDuration(100).start()

  我们可以利用动画的特性来实现一些动画不能实现的效果。还拿scrollTo来说,我们也想模仿Scroller来实现View的弹性滑动,那么利用动画的特性,我们可以采用如下方式来实现:

final int startX = 0;final int deltaX = 100;ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);animator.addUpdateListener(new AnimatorUpdateListener(){   @Override   public void onAnimationUpdate(ValueAnimator animator){        float fraction = animator.getAnimatedFraction();        mButton1.scrollTo(startX+(int)(deltaX*fraction),0);   }});animator.start();

  上述代码中,我们的动画本质上没有作用于任何对象上,它只是在1000ms内完成了整个动画过程。利用这个特性,我们就可以在动画的每一帧到来时获取完成的比例,然后再根据这个比例计算当前View所要滑动的距离。注意,这里的滑动针对的是VIew的内容而非View本身。可以发现,这个方法的思想其实和Scroller比较类似,都是通过改变一个百分比配合scrollTo方法来完成View的滑动。

3.View的滑动冲突处理

3.1常见的滑动冲突场景

3.1.1 外部滑动方向和内部滑动的方向不一致

这里写图片描述
  这种情况我们经常遇见,比如使用viewpager+listview时,在这种效果中,可以通过左右滑动切换页面,而每一个页面往往又是一个listview,本来在这种情况下是有冲突的,但是viewpager内部处理了这个滑动冲突,因此采用viewpager我们无需关注这个问题,如果我们采用的不是viewpager而是ScrollView等,那么必须手动处理滑动冲突,否则内外两层只能有一层滑动,那就是滑动冲突。另外内部左右滑动,外部上下滑动也同样属于该类。

3.1.2 外部滑动方向和内部滑动方向一致

这里写图片描述
  这种情况就比较复杂,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题,因为当手指开始滑动的时候,系统无法知道用户到底是想让那一层动,所以当手指滑动的时候就会出现问题,要么只能一层动,要么内外两成动的都很卡顿。

3.2 滑动冲突的解决

3.2.1 外部拦截法

  所谓的外部拦截法就是指点击事件都要先经过父容器的拦截处理,如果父容器需要此事件拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,这种方法的伪代码如下:

 public boolean onInterceptTouchEvent(MotionEvent event) {        boolean intercepted = false;        int x = (int) event.getX();        int y = (int) event.getY();        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN: {                intercepted = false;                break;            }            case MotionEvent.ACTION_MOVE: {                if(父容器拦截的规则){                    intercepted=true;                }else{                    intercepted=false;                }                break;            }            case MotionEvent.ACTION_UP: {                intercepted = false;                break;            }            default:                break;        }        mLastXIntercept=x;        mLastYIntercept=y;        return intercepted;    }

  上面的代码差多就是外部拦截的通用模板了,在onInterceptTouchEvent方法中,
  首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截事件,因为一旦父容器拦截了ACTION_DOWN这个事件,那么后续的ACTION_MOVE和ACTION_UP事件将直接交给父容器处理,这个时候事件没法继续传递给子元素了;
  然后是ACTION_MOVE这个事件,这个事件可以根据需要决定是否拦截,如果父容器需要拦截就返回true,否则返回false;
  最后是ACTION_UP这个事件,这里必须返回false,因为这个事件本身也没有太多意义。

3.2.2 内部拦截法

  内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器去处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。他的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:

 public boolean onInterceptTouchEvent(MotionEvent event){        int x = (int) event.getX();        int y = (int) event.getY();        switch (event.getAction()){            case MotionEvent.ACTION_DOWN:                parent.requestDisallowInterceptTouchEvent(true); //父布局不拦截此事件                break;            case MotionEvent.ACTION_MOVE:                int deltaX = x - mLastX;                int deltaY = y - mLastY;                if(父容器需要此类点击事件){                    parent.requestDisallowInterceptTouchEvent(false);                }                break;            case MotionEvent.ACTION_UP:                break;            default:                break;        }        mLastX = x;        mLastY = y;        return super.dispatchTouchEvent(event);    }

   以上是内部拦截法的典型代码,当面对不同滑动策略的时候只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理之外,父元素也需要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptionTouchEvent(false)方法时,父元素才能继续拦截所需的事件。

原创粉丝点击