关闭

关于Android的Scroller类和View

标签: uiandroid
329人阅读 评论(0) 收藏 举报
分类:

关于Android Scroller和View

android程序中,大部分比较炫酷UI,都是基于SCroller或者是动画来实现的。本文主要讲解下Scroller类是如何配合View组件来使用的。

我们首先来看下View.scrollTo(int x,int y)和View.ScrollBy(int dx,int dy)两个方法:这两个方法其实很简单:

  • scrollTo方法是滑动到一个X,Y(参数中的)坐标上去,传入的参数X,Y是具体的屏幕的坐标的数值.
  • scrollBy方法是从在当前的坐标数值上,滑动传入的参数dx,dy的那么多的距离。

    说白了一个是滑动到具体的坐标点,一个是从当前内容的坐标开始滑动一个传入的参数的距离。注意,这里说的滑动和动画中的滑动不是一个概念:动画中的滑动是将View组件整体的坐标值改变来进行滑动,改变了View组件在屏幕中的坐标点,而Scrollto或者ScrollBy中的滑动,是滑动的组件的内容。所以嘛,一般能用到scrollto或者scrollby这两个方法的组件,肯定都是内容比较大,超出屏幕显示的。比如android常用的scrollView或者HorizontalScrollView 组件,就是基于这两个方法来的。

    那这两个方法一般在什么情形下去使用呢?其实用膝盖想也知道是onTouchEvent();在onTounchEvent()中使用监听事件计算手势划过的距离,然后调用scrollBy()方法,然View的内容进行滑动。关于滑动参数的正负型:x轴,正数代表内容向左滑动,负数代表内容向右滑动。y轴:正数代表内容向上滑动,负数代表向下滑动。

    了解这两个方法之后,我们来说说View.getScrollX()和View.getScrollY()的含义。其实很简单,scrollX就是代表View左边缘距离手机屏幕左边的宽大小,scrollY就是代表View上边缘距离手机屏幕右边的大小(感谢任玉刚主席书中的解释!)所以,可以这样理解:从左向右滑动的时候,getScrollX为负值,反之亦然;从上向下滑动的时候,getScrollY为正数值,反之亦然。

    好吧,现在扯了那么多,说好的Scroller类呢?这就开始说Scroller类,其实上面的篇章将的都是View关于滑动的动作,而Scroller类就是辅助计算View滑动的,也就是说,Scroller类是一个辅助计算类。也许你会问:这不是搞笑吗?我们既然能在onTouchEvent中计算滑动的距离,还需要Scroller类吗?其实,ScrollBy或者ScrollTo确实都是配合手势来用的,但是这些滑动都是瞬间的滑动,并没有给用户滚动的过程,其实造成的用户体验不是很好,所以需要使用到scroller类。

    我们可以思考如下场景:

    • 1.比如一个侧滑菜单,我只是随意滑动了一下,就要让侧滑的菜单从侧面平缓地显示出来呢?(所有的侧滑都是这样的)
    • 2.比如一个竖直的页面,是滚动类型的,内容分为A,B两块,A在上,B在下,现在我随意地轻轻地滑动,而且希望能够根据滑动的速度,A完全消失,让B显示,又改如何做呢?(比如360的软件详情页面)

    这些问题都是涉及到比较复杂的滑动计算的,而这些滑动计算的功能,都需要Scroller类来帮助我们计算。

    其实刚刚接触到Scroller类的人都会觉得很难理解:其实我们要思考清楚两个问题:

    1. Scroller是怎样去计算辅助View的滑动的?
    2. Scroller是的事件怎么样被出发的?
      我们从一段示例的代码中来看抛砖引玉:
package slidingmenu.dreamfly.org.slidingmenu.custom.app;

import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.BounceInterpolator;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.OverScroller;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.Scroller;

import slidingmenu.dreamfly.org.slidingmenu.R;

/**
 * Created by asus on 2015/10/20.
 */
public class DetailRootLayout  extends LinearLayout {

    private View mTop;
    private View mBottom;

    private int mTopViewHeight;
    private OverScroller mScroller;
    private VelocityTracker mVelocityTracker;
    private int mTouchSlop;
    private int mMaximumVelocity, mMinimumVelocity;

    private float mLastY;
    private boolean isDragging;

    public DetailRootLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOrientation(LinearLayout.VERTICAL);
        this.mVelocityTracker=VelocityTracker.obtain();
        this.mScroller=new OverScroller(context);
        this.mTouchSlop=ViewConfiguration.get(context).getScaledTouchSlop();
        this.mMaximumVelocity=ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        this.mMinimumVelocity=ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
    }


     @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
          super.onSizeChanged(w,h,oldw,oldh);
          this.mTopViewHeight=this.mTop.getMeasuredHeight();
          Log.i("lzw","topHeight"+this.mTopViewHeight);
    }


    /**
     * startScroll(滑动的起点的X,滑动的起点的Y,滑动的最终距离X,滑动的最终距离Y)
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event){
         this.mVelocityTracker.addMovement(event);
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 this.mLastY = event.getY();
                 if (this.mScroller.isFinished()) {
                     this.mScroller.abortAnimation();
                 }
                 break;
             case MotionEvent.ACTION_MOVE:
                 float disY = event.getY() - this.mLastY;
                 if (!this.isDragging && Math.abs(disY) > this.mTouchSlop) {
                     this.isDragging = true;
                 }
                 if (this.isDragging) {
                     this.scrollBy(0, -(int) disY);
                     this.mLastY=event.getY();
                 }
                 break;
             case MotionEvent.ACTION_UP:
                 this.isDragging = false;
//                 this.mVelocityTracker.computeCurrentVelocity(1000,this.mMaximumVelocity);
//                 float velocityY=this.mVelocityTracker.getYVelocity();
//                 //向下滑动,速率的计算值肯定是负数
//                 if(Math.abs(velocityY)>this.mMinimumVelocity){
//                      this.fling(-(int)velocityY);
//                 }
                 float disTmp=event.getY()-this.mLastY;
                 this.mScroller.startScroll(this.mScroller.getFinalX(),
                                 this.getScrollY(),0,-(int)disTmp);
                 this.invalidate();     
                 break;
         }
         return(true);
    }


    /**
     * fling函数
     * startX:开始滑动的X起点
     * startY:开始滚动的Y起点
     * velocityX:滑动的速度X
     * velocityY:滑动的速度Y
     * minx:X方向的最小值
     * maxX:X方向的最大值
     * minY:Y方向的最小值
     * maxY:Y方向的最大值
     * @param velocity
     */
    private void fling(int velocity){
         this.mScroller.fling(0,this.getScrollY(),0,velocity,0,0,0,this.mTopViewHeight);
         this.invalidate();
    }


    /**
     * 在fling或者startScroll方法后,调用invalidate方法后执行的函数
     * scroller.getCurY() 返回当前Y方向的偏移
     */
    @Override
    public void computeScroll() {
        if(this.mScroller.computeScrollOffset()){
              this.scrollTo(0,this.mScroller.getCurrY());
              invalidate();
        }
    }
  /**
     *
     * @param x
     * @param y
     */
    @Override
    public void scrollTo(int x,int y){
            if(y<0) {
                y = 0;
            }
            //指定一个滑动的上限
            if(y>this.mTopViewHeight){
               y=mTopViewHeight;
            }
            if(y!=this.getScrollY()){
                  super.scrollTo(x,y);
            }
    } @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        this.mTop=this.findViewById(R.id.topView);
        this.mBottom=this.findViewById(R.id.bottomView);
    }




}

 <slidingmenu.dreamfly.org.slidingmenu.custom.app.DetailRootLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
       <RelativeLayout
           android:id="@+id/mTopView"
           android:layout_width="match_parent"
           android:background="#4400ff00"
           android:layout_height="400dp">
             <TextView
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
                 android:textSize="22sp"
                 android:gravity="center"
                 android:text="软件介绍"
                 />
       </RelativeLayout>
       <RelativeLayout
           android:id="@+id/mBottomView"
           android:layout_width="match_parent"
           android:background="#4400ff00"
           android:layout_height="400dp">
             <TextView
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
                 android:textSize="22sp"
                 android:gravity="center"
                 android:text="软件详情"
                 />
       </RelativeLayout>
</slidingmenu.dreamfly.org.slidingmenu.custom.app.DetailRootLayout>

从上面的XML和java代码中,我们可以看到,自定义的LinearLayout的是一个模仿ScrollView类的自定义控件。这个LinearLayout含有两个布局:mTop和mBottom。其中,mTop布局和mBottom布局不能同时显示(mTop可以全部显示,mBottom只能显示一个部分)当我们需要手指向下滑动的时候,我们需要这个自定义的LinearLayout经过滑动后,将mTop完全隐藏,将mBottom完全显示。这时候,就需要Scroller类来辅助滑动了:我们仔细看这段代码中的这几个部分:

     case MotionEvent.ACTION_UP:
                 this.isDragging = false;
                 float disTmp=event.getY()-this.mLastY;
                 this.mScroller.startScroll(this.mScroller.getFinalX(),
                                 this.getScrollY(),0,-(int)disTmp);

                 break;

这里监听到手势抬起的动作,然后计算move的最后一点的动作y和up动作的Y的差值,然后调用Scroller的startScroll函数。这里先讲下startScroll函数的参数都是什么含义:

  •  public void startScroll (int startX, int startY, int dx, int dy, int duration)

      以提供的起始点和将要滑动的距离开始滚动。

    • startX 水平方向滚动的偏移值,以像素为单位。正值表明滚动将向左滚动(滑动的起点)

    • startY 垂直方向滚动的偏移值,以像素为单位。正值表明滚动将向上滚动(滑动的终点)

    •   dx 水平方向滑动的距离,正值会使滚动向左滚动

    •   dy 垂直方向滑动的距离,正值会使滚动向上滚动

    • duration 滚动持续时间,以毫秒计。默认是以250ms计算的

可以看到,这个函数并没有实现滑动的动作,而是记录了滑动的距离(在Scroller类的源代码中可以看到)那滑动的动作是如何实现呢?当然靠view啦,我们看接下来的代码:在startScroll之后,我们调用了 this.invalidate()方法,这个方法的意思是请求View重新绘制,这时候,view就会调用computeScroll()这个方法,这个方法在View中是空,就是等着子类来覆写这个方法。于是,我们在这里覆写方法的代码为:

  @Override
    public void computeScroll() {
        if(this.mScroller.computeScrollOffset()){
              this.scrollTo(0,this.mScroller.getCurrY());
              invalidate();
        }
    }

这个方法里面我们使用了Scroller类的computeScrollOffset()方法,和scroller类的getCurrY()方法。这都是什么意思呢?其实很简单,在computeScrollOffset()方法中,根据startScroll方法中的被赋值的参数来进行计算现在滑动的距离,这个方法返回的是boolean数值,代表是否计算完成了,何时计算完成了呢?其实也是在startScroll中被赋值的。computeScrollOffset()不断的计算更新当前应该滑动到的curX或者curY(也就是scroller.getCurX()/getY())的数值,直到computeScrollOffset()的数值已经达到了startScroll中的指定的数值,就会返回false。
因此我们需要有这样一个不断循环的逻辑 rcomputeScroll()->getCurX/getCurY->invalidate(),来完成View的内容上的滑动/那么我们怎么控制滑动的最大间距呢?可以同通过覆写scrollTo方法:

  /**
     *
     * @param x
     * @param y
     */
    @Override
    public void scrollTo(int x,int y){
            if(y<0) {
                y = 0;
            }
            //指定一个滑动的上限
            if(y>this.mTopViewHeight){
               y=mTopViewHeight;
            }
            if(y!=this.getScrollY()){
                  super.scrollTo(x,y);
            }
    }

来控制滑动的最大距离。
这就是,使用scroller类来实现View平缓滑动的一种方式,总结一下:View.scrollBy和View.scrollTo都是瞬时过度的,要像让View平缓过度,我们就需要利用Scroller的辅助计算,把一些复杂的过程呢个计算交给scroller,把然后配合scroller的计算结果(currY/currX)来调用View.scrollTo或者scrollBy方法。

接下来我们,看看借助scroller实现平缓滑动的其他方式(在文档中被注释掉的)那部分代码:

                this.mVelocityTracker.computeCurrentVelocity(1000,this.mMaximumVelocity);
                 float velocityY=this.mVelocityTracker.getYVelocity();                 //向下滑动,速率的计算值肯定是负数
                if(Math.abs(velocityY)>this.mMinimumVelocity){
                      this.fling(-(int)velocityY);

可以看到,实现View的平缓滑动,可以是使用scroller.fling()方法,我们首先通过mVelocityTracker(速度追踪器类)来计算滑动的速度,然后调用scroller.fling()方法:我们来看看fling方法的参数是都代表什么意思?

*  public void fling (int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)

  在fling(译者注:快滑,用户按下触摸屏、快速移动后松开)手势基础上开始滚动。滚动的距离取决于fling的初速度。

参数

  • startX 滚动起始点X坐标
  • startY 滚动起始点Y坐标
  • velocityX 当滑动屏幕时X方向初速度,以每秒像素数计算
  • velocityY 当滑动屏幕时Y方向初速度,以每秒像素数计算
  • minX X方向的最小值,scroller不会滚过此点。
  • maxX X方向的最大值,scroller不会滚过此点。
  • minY Y方向的最小值,scroller不会滚过此点。
  • maxY Y方向的最大值,scroller不会滚过此点。

    所以,fling方法主要也是做数据记录,记录下这些数据,之后呢?我们再次调用 invalidate();来请求重新绘制,然后和之前的逻辑是一样的了,仍然是循环逻辑,然后不断地调用View.scrollTo/By等方法来实现View的缓慢滑动。有两点与之前的方法不同的是:这里可以设置根据手势设置View滑动的速度,比较灵活一点;同时也可以不用覆写scrollTo的方法,去设置最大上限,因为在fling方法中已经设置过了。
    好吧,现在我们已经了解了scroller的用法了,总结下:

    • 1.View有两种滑动的方式:scrollTo和scrollBy两种方法,这两个方法都是实现view内容滑动的主要方法,但是,这两个方法都是瞬时完成的,没有什么过渡的阶段。
    • 2.Scroller类是一个辅助View滑动的计算类,主要做数据存储和数据计算,通过覆写View.computeScroll()方法来,调用Scroller.copmuteOffset()和不断地请求View重新绘制,这样循环的逻辑,来实现View的有过程的滑动。


    Q&A:

    1. View.getScrollY(),View.getScrollX() 和View.getX(),View.getY() 区别?这个问题其实就是Scroller滑动和View的动画的区别,如果我们实现了View内容的滑动(比如借助scroller类),那getScrollX/或者是getScrollY是经常变化的,getX/Y是不变化的。
    2. 借助scroller类实现滑动的时候,View.scrollTo() 的参数为什么使用Scroller.getCurX/Y()?这个问题其实在文中已经说了,每次循环的逻辑的都是先调用Scroller.copmuteOffert()方法,在这个方法中,Scroller类会根据startScroll或者是fling中的方法去计算下一个时间段中的要滑动到地方,然后赋值给内部的类curX/curY,之后调用View.scrollTo/scrollBy方法的时候,在将计算好的额curX/curY取出来就可以了
    3. View.getScrollY() 和 Scroller.getCurY() 的有何区别?View.getScrollY()方法代表是组件边缘和手机屏幕的距离,Scroller.getCurY()是在滑动中,下一步滑动要去的坐标的位置,两者基本没有什么太多关联

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:15499次
    • 积分:415
    • 等级:
    • 排名:千里之外
    • 原创:26篇
    • 转载:4篇
    • 译文:0篇
    • 评论:2条
    文章分类
    最新评论