Android滚动视图Scroller

在开发过程中,我们往往需要通过自定义View来实现平滑滚动的视图,这就需要用到Scroller了。在具体介绍Scroller之前,先讲解一下相关的基础知识,比如:坐标系,scrollTo()方法,scrollBy()方法,getScrollX()方法和getScrollY()方法。

一. 基础知识

1. 坐标系

Android中有两种坐标系,根据坐标原点的不同代表不同的含义,分别是Android坐标系(绝对坐标系)和视图坐标系(相对坐标系)。Android坐标系中的坐标原点是屏幕左上角的顶点,而视图坐标系的原点则是父视图的左上角的顶点。由此可见,Android坐标系描述的是视图在整个屏幕中的位置,而视图坐标系描述的是子视图和父视图的关系,即子视图处于父视图的哪个位置上。

2. scrollTo(int x, int y)方法

使用scrollTo方法时一定要记住,在容器视图ViewGroup中调用scrollTo或scrollBy方法,移动的是子视图内容的位置而不是当前容器视图的位置。

scrollTo方法:始终相对于视图的初始位置移动视图。scrollTo接收两个参数,分别指明水平方向和垂直方向移动的距离。具体的方向与值的正负有关:

(1)x > 0,相对于初始位置向x轴负方向移动;

(2)x < 0,相对于初始位置向x轴正方向移动;

(3)y > 0,相对于初始位置向y轴负方向移动;

(4)y < 0,相对于初始位置向y轴正方向移动;

对于初学者来说,可能会认为x大于0,就应该是向x轴的正方向移动。之所以理解上会产生差异,原因在于对移动的具体“物体”发生了混淆。下面通过一个现实生活中的例子来区分这两种差异:

桌上有一张白纸,在上面我们可以绘制任何内容。此时,拿过来一个手机屏幕大小的玻璃,放在白纸上。这块玻璃可以看做一个屏幕坐标系,假设只有处于玻璃正下方的内容,我们才能够看到。

接着,我们在玻璃正下方的中间区域绘制一个红色的正方形,然后我们希望这个正方形能够往屏幕的右下方各移动10的距离。那么有两种做法:

做法一:玻璃不动,白纸(画布)向右下方移动10个距离单位;

做法二:白纸不动,玻璃(屏幕)向左上方移动10个距离单位;

而scrollTo的使用可以理解为做法二。

3. scrollBy(int x, int y)方法:相对于视图的上一次位置移动视图。其他与scrollTo方法完全一致。

4. getScrollX()方法:获取视图相对于视图初始位置的水平方向移动的距离(这个初始位置很重要)。

假设ViewA是ViewGroupA的子视图,且ViewA可以跟随手指左右移动。初始位置为图一。

(1)手指按住ViewA从初始位置向左移动,此时通过getScrollX()获取到的滚动距离是10(坐标系正向移动10),见图二;

(2)手指按住ViewA从初始位置向右移动,此时通过getScrollX()获取到的滚动距离是-10(坐标系负向移动10),见图三;

所以,getScrollX()获取到的值也可以理解为移动屏幕(坐标系)的值。

5. getScrollY()方法:获取视图相对于视图初始位置的垂直方向移动的距离,其他与getScrollX方法完全一致。

 

二. Scroller

虽然我们可以通过scrollTo和scrollBy轻松的移动子视图,但是移动的过程都是瞬时完成的。在某些场景下对用户可能并不友好,而Scroller类就可以帮助我们实现平滑的滚动效果。Scroller类的实现原理实际上也是通过在ACTION_MOVE事件中不断的获取微小的偏移量,每个偏移量通过scrollBy来完成移动,这样整体上就可以获得平滑的移动效果。

使用Scroller类可以分为以下几个步骤:

(1)创建Scroller实例;

(2)重写computeScroll()方法,模拟滑动;

computeScroll方法是使用Scroller类的核心,系统在绘制View时会在draw方法中调用此方法。常用模板代码如下:

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {  // 判断是否完成了滚动过程
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  // 滚动到将要滚动的位置
        invalidate();  // 触发重绘->调用draw方法->调用本方法,达到不断更新视图滚动位置的效果
    }
}

上面代码中,getCurrX()方法和getCurrY()方法可以用于获取当前滚动的坐标。

(3)调用Scroller实例的startScroll()方法,启动平滑移动的过程;

startScroll具有如下两个重载的方法:

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

startX,startY是视图滚动的起始位置,dx,dy则是滚动到目标位置的偏移量,duration则是滚动的持续时间。

下面是一个通过Scroller来创建一个容器组件,模拟ViewPager切换子页面的效果(例子来源于郭霖大神的博客:Android Scroller完全解析,关于Scroller你所需知道的一切,非常感谢~~)。代码如下:

public class ScrollerLayout extends ViewGroup {
    private static final String TAG = "ScrollerLayout";

    /**
     * 用于完成滚动操作的实例
     */
    private Scroller mScroller;

    /**
     * 可视为可滑动一整页的,用户手指移动的最小距离(像素点),通过系统提供的ViewConfiguration获取
     */
    private int mTouchSlop;

    /**
     * 手指按下时的横坐标
     */
    private float mXDown;

    /**
     * 手指移动过程中的横坐标
     */
    private float mXMove;

    /**
     * 上一次手指移动过程中的横坐标
     */
    private float mXLastMove;

    /**
     * 界面可滚动的左边界
     */
    private int leftBorder;

    /**
     * 界面可滚动的右边界
     */
    private int rightBorder;


    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);

        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        if (b) {
            int childCount = getChildCount();
            for (int j = 0; j < childCount; j++) {
                View childView = getChildAt(j);
                childView.layout(j * childView.getMeasuredWidth(), 0, (j + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
            leftBorder = getChildAt(0).getLeft();
            rightBorder = getChildAt(childCount - 1).getRight();
            Log.d(TAG, "onLayout, leftBorder is " + leftBorder + ",  rightBorder is " + rightBorder);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mXLastMove = mXDown;
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mXDown);
                mXLastMove = mXMove;
                if (diff > mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // getScroll():获取手指移动,触发视图相对于视图初始位置的移动的距离(这个初始位置很重要)
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();
                int scrolledX = (int)(mXLastMove - mXMove);  // 获取当前手指相对上一次移动的距离(若为负值,表示向右移动;若为正值,表示向左移动)
                Log.d(TAG, "XXX," + getScrollX() + " " + scrolledX + " " + getWidth());  // 当前的位置 + 将要移动的位置差 = 将要出现的位置
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                    scrollTo(rightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                mXLastMove = mXMove;
                break;
            case MotionEvent.ACTION_UP:
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

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

备注:

(1)ViewConfigurationCompat.getScaledPagingTouchSlop可以用于获取一个整型的像素值,是一个系统的视图常量,表示当用户手指移动了该距离后,可以视为滑动一屏。在onInterceptTouchEvent方法中,如果用户的手指滑动距离超过该值,那么说明应该滚动,由ScrollerLayout响应事件,拦截事件向子视图的传递。

(2)getScrollX() + scrolledX < leftBorder;getScrollX()获取当前视图的水平位置,scrolledX是计算得到的将要滑动的偏移量,getScrollX() + scrolledX得到的是子视图将要滑动的距离。如果该值小于leftBorder,说明视图将要向右离开初始位置,此时调用scrollTo(leftBorder, 0);来限制子视图的向右滚动。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值