高级UI<第四十四篇>:Android Scroller详解

滚动视图的方法有两种:scrollToscrollBy,而Scroller就是它们的辅助工具类,所以Scroller是学好高级UI必不可少的一课。

(1)scrollTo、scrollBy、getScrollX、getScrollY

view的内容本身具备滚动的方法,其中滚动方法如下:

  • scrollTo:相对于初始位置移动
  • scrollBy:相对于上次移动的最后位置移动

这两个方法特别需要注意以下几点:

  • 两者移动的都是view的内容,view本身是不移动的,所以getX和getY的值不会受到这两个方法的影响;
  • 不要再在onDraw中调用这两个方法,避免onDraw方法被重复执行,因为一旦调用这两个方法view会被重绘,onDraw方法会再次执行。

view内容滚动的方法有了,那么该如何获取view内容被滚动的距离呢?看以下两个方法:

  • getScrollX:获取view的内容在X轴滚动的距离
  • getScrollY:获取view的内容在Y轴滚动的距离

以上只说到view内容的滚动,那么view本身的移动用什么方法呢?

答:setXsetY方法。

本文的重点内容是Scroller,这个辅助类的作用不是view本身的移动,而是view内容的滚动,下面开始简单说明一下Scroller辅助类。

(2)熟悉Scroller的构造方法
//默认插值器是ViscousFluidInterpolator
Scroller mScroller = new Scroller(mContext);

//指定一个插值器
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator());

//指定一个插值器,第三个参数表示是否开启“飞轮”效果,也就是多次滚动时速度叠加
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator(), false);
(3)熟悉插值器
图片.png

Scroller其实就是在scrollTo(x, y)scrollBy(x, y)的基础上添加滚动效果,滚动效果是一个动画,当我们new一个Scroller对象时,就已经指定了一个插值器,下面来说明一下各种插值器:

  • ViscousFluidInterpolator:这是一个默认插值器,当构造Scroller时,如果不传递插值器或者插值器为null时,系统默认使用ViscousFluidInterpolator插值器。
  • AccelerateDecelerateInterpolator:在动画开始与结束的时候速率改变比较慢,在中间的时候速率较快。
  • AccelerateInterpolator:在动画开始的地方速率改变比较慢,然后开始加速。
  • AnticipateInterpolator:开始的时候向后然后向前甩。

  • AnticipateOvershootInterpolator:开始的时候向后然后向前甩一定值后返回最后的值。

  • BounceInterpolator:反弹插值器。

  • CycleInterpolator:动画循环播放特定的次数,速率改变沿着正弦曲线。

  • DecelerateInterpolator:在动画开始的地方快然后慢。

  • LinearInterpolator:以常量速率改变。

  • OvershootInterpolator:向前甩一定值后再回到原来位置。

  • PathInterpolator:路径插值器,我们可以按照自己想要的轨迹滚动。

    PathInterpolator(Path path)
    PathInterpolator(float controlX, float controlY)
    PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2)

  • FastOutLinearInInterpolator:MaterialDesign基于贝塞尔曲线的插补器效果:依次慢慢快。

  • FastOutSlowInInterpolator:基于贝塞尔曲线的插补器效果:依次慢快慢
  • LinearOutSlowInInterpolator:基于贝塞尔曲线的插补器效果:依次快慢慢

以上的插值器运用比较广泛,在Scroller中设置一个插值器可以优化滚动的效果。

(4)Scroller滑动辅助类的基本方法

Scroller本身不会去滚动view,它只是一个滚动计算辅助类,用于跟踪控件滑动的轨迹,只相当于一个滚动轨迹记录工具,最终还是通过View的scrollTo、scrollBy方法实现view的滚动。

  • getCurrX()

获取mScroller当前水平滚动的位置

  • getCurrY

获取mScroller当前竖直滚动的位置

  • getFinalX

获取mScroller最终停止的水平位置

  • getFinalY

获取mScroller最终停止的竖直位置

  • startScroll()

开始滚动动画:
startX:滚动的x方向起始点
startY:滚动的y方向起始点
dx:x方向的偏移量
dy:y方向的偏移量
duration:滚动所消耗的时间,默认为250毫秒

startScroll(int startX, int startY, int dx, int dy)
startScroll(int startX, int startY, int dx, int dy, int duration)
  • fling()

惯性滑动,参数如下:

startX:滚动起始点x
startY:滚动起始点y
velocityX:x轴方向的速度
velocityY:y轴方向的速度
minX:x轴最小滚动距离(注意直角坐标系)
maxX:x轴最大滚动距离(注意直角坐标系)
minY:y轴最小滚动距离(注意直角坐标系)
maxY:y轴最大滚动距离(注意直角坐标系)

  • computeScrollOffset()

判断滚动动画是否结束:
true:滚动尚未完成
false:滚动已经完成

(5)Scroller实现滚动效果
public class TestView extends View {

    private float mDownX = 0;
    private float mDonwY = 0;
    private float move_x = 0;
    private float move_y = 0;
    private int finalX = 0;
    private int finalY = 0;

    private Paint mPaint;

    private Scroller mScroller;

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

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context mContext){
        mPaint = new Paint();
        mPaint.setTextSize(80);

        mScroller = new Scroller(mContext);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText("我是中国人!", 0, 100, mPaint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //标志着第一个手指按下
                mDownX = x;//获取按下时x坐标值
                mDonwY = y;//获取按下时y坐标值
                break;
            case MotionEvent.ACTION_MOVE:
                //按住一点手指开始移动
                move_x = mDownX - x;//计算当前已经移动的x轴方向的距离
                move_y = mDonwY - y;//计算当前已经移动的y轴方向的距离

                //开始滚动动画
                //第一个参数:x轴开始位置
                //第二个参数:y轴开始位置
                //第三个参数:x轴偏移量
                //第四个参数:y轴偏移量
                mScroller.startScroll(finalX, finalY, (int) move_x, (int) move_y, 0);
                invalidate();//目的是重绘view,是的执行computeScroll方法
                break;
            case MotionEvent.ACTION_UP:
                finalX = mScroller.getFinalX();
                finalY = mScroller.getFinalY();

            break;

        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){//判断滚动是否完成,true说明滚动尚未完成,false说明滚动已经完成
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//将view直接移动到当前滚动的位置
            invalidate();//触发view重绘
        }
    }
}

效果如下:

51.gif

当发生ACTION_MOVE事件时,执行startScroll方法开始滚动view,由于ACTION_MOVE事件发生的特别频繁,所以startScroll方法的最后一个参数设置为0ms。

当然,可以将startScroll方法放在ACTION_UP事件中执行,调整代码:

@Override
public boolean onTouchEvent(MotionEvent event) {

    float x = event.getX();
    float y = event.getY();
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            //标志着第一个手指按下
            mDownX = x;//获取按下时x坐标值
            mDonwY = y;//获取按下时y坐标值
            break;
        case MotionEvent.ACTION_MOVE:
            //按住一点手指开始移动
            move_x = mDownX - x;//计算当前已经移动的x轴方向的距离
            move_y = mDonwY - y;//计算当前已经移动的y轴方向的距离
            
            break;
        case MotionEvent.ACTION_UP:
            finalX = mScroller.getFinalX();
            finalY = mScroller.getFinalY();

            //开始滚动动画
            //第一个参数:x轴开始位置
            //第二个参数:y轴开始位置
            //第三个参数:x轴偏移量
            //第四个参数:y轴偏移量

            mScroller.startScroll(finalX, finalY, (int) move_x, (int) move_y, 3000);
            invalidate();//目的是重绘view,是的执行computeScroll方法

        break;

    }
    return true;
}

代码的意思是:从一个坐标到另一个坐标的滑动需要3秒时间,当手指松开时开始执行滚动动画,动画时长为3秒。

效果如下:

52.gif
(6)Scroller实现惯性滚动效果

惯性滚动是指,手指松开view后,根据当前速度再滑动一段距离,就跟惯性类似。

实现惯性滚动效果的方法是fling(),执行fling()方法的时机是MotionEvent.ACTION_UP事件,代码实现如下:

public class TestView extends View {

    //惯性滑动速度追踪类
    private VelocityTracker velocityTracker;
    private float mDownX = 0;
    private float mDonwY = 0;
    private float move_x = 0;
    private float move_y = 0;
    private int finalX = 0;
    private int finalY = 0;
    private int xVelocity = 0;
    private int yVelocity = 0;

    private Paint mPaint;

    private Scroller mScroller;
    private OverScroller mOverScroller;

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

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context mContext){
        mPaint = new Paint();
        mPaint.setTextSize(80);
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(50);
        mScroller = new Scroller(mContext);
        mOverScroller = new OverScroller(mContext);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText("我是中国人!", 0, 100, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //标志着第一个手指按下
                mDownX = x;//获取按下时x坐标值
                mDonwY = y;//获取按下时y坐标值

                //创建惯性滑动速度追踪类对象
                velocityTracker = VelocityTracker.obtain();



                break;
            case MotionEvent.ACTION_MOVE:
                //按住一点手指开始移动
                move_x = mDownX - x;//计算当前已经移动的x轴方向的距离
                move_y = mDonwY - y;//计算当前已经移动的y轴方向的距离

                //开始滚动动画
                //第一个参数:x轴开始位置
                //第二个参数:y轴开始位置
                //第三个参数:x轴偏移量
                //第四个参数:y轴偏移量
                if(mScroller.isFinished()){
                    mScroller.startScroll(finalX, finalY, (int) move_x, (int) move_y, 0);
                }
                invalidate();//目的是重绘view,是的执行computeScroll方法


                //将事件加入到VelocityTracker类实例中
                velocityTracker.addMovement(event);
                //计算1秒内滑动的像素个数
                velocityTracker.computeCurrentVelocity(1000);
                //X轴方向的速度
                xVelocity = (int) velocityTracker.getXVelocity();
                //Y轴方向的速度
                yVelocity = (int) velocityTracker.getYVelocity();

                break;
            case MotionEvent.ACTION_UP:

                //获取认为是fling的最小速率
                int mMinimumFlingVelocity=  ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity() / 10;


                if (Math.abs(xVelocity) >= mMinimumFlingVelocity || Math.abs(yVelocity) > mMinimumFlingVelocity) {
                    Log.d("yunchong", "触发惯性滑动");
                    mScroller.fling(getScrollX(), getScrollY(), -xVelocity, -yVelocity, -getWidth()+100, 0,  -getHeight()+100,  0);
                } else {//缓慢滑动不处理
                }


                finalX = mScroller.getFinalX();
                finalY = mScroller.getFinalY();

                velocityTracker.recycle();
                velocityTracker.clear();
                velocityTracker = null;

                break;


            case MotionEvent.ACTION_CANCEL:

                velocityTracker.recycle();
                velocityTracker.clear();
                velocityTracker = null;

                break;

        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){//判断滚动是否完成,true说明滚动尚未完成,false说明滚动已经完成
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//将view直接移动到当前滚动的位置
            invalidate();//触发view重绘
        }
    }
}

效果展示:

52.gif

[本章完...]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容