zhiweiusetc的专栏

想大神努力

关于Android的Scroller类和View

关于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()是在滑动中,下一步滑动要去的坐标的位置,两者基本没有什么太多关联

阅读更多
文章标签: ui android
个人分类: android 界面开发
上一篇android handler 和Looper 的理解
下一篇# git团队开发流程小结
想对作者说点什么? 我来说一句

Android Scroller实现View弹性滑动Demo

2017年08月29日 725KB 下载

android Scroller粗暴分析

2015年01月08日 116KB 下载

Android应用源码高仿微信UI

2015年05月29日 4.01MB 下载

scrolldemo

2015年11月29日 5.39MB 下载

仿ViewPager

2015年01月20日 3.05MB 下载

Android之Scroller(滑动)完全解析

2015年03月05日 5.05MB 下载

MyGuideView1

2013年08月16日 1.28MB 下载

没有更多推荐了,返回首页

关闭
关闭