Android 中 scrollTo | scrollBy | Scroller 的使用

版权声明:本文出自阿钟的博客,转载请注明出处: https://blog.csdn.net/a_zhon/article/details/78917045

Android Scroller完全解析,关于Scroller你所需知道的一切

本文基于郭神给出的教程(原文地址如上),在结合自己的编码总结这个几个东西的使用。

先来说 scrollTo()与scrollBy()的使用与区别:

这两个函数都是在View.java中,所以这两个都可以对View视图进行滚动。
阅读这两个函数的源码就可以找出它们的区别了。

这里写图片描述

  • 可以发现scrollBy()最终调用的还是scrollTo(),只是对参数进行了改变;mScrollX、mScrollY代表的就是View x、y轴当前滑动的距离。
  • 通过分析我们可以看出一点:
    • scrollTo() 是将View滚动到指定的一个位置A(x,y)
    • scrollBy() 是将View从上次滚动的位置 继续滚动一定的距离
  • 这两个函数一般都是作用于ViewGroup的,滚动其内部子View;如果作用于View上的话 效果会很糟糕的(不信你试试)

区别说完了,那就动手来写代码实践吧!

  • 效果图

这里写图片描述

使用一个LinearLayout内部放置两个Button即可,用来分别调用scrollBy()scrollTo()函数。

  • scrollTo
final LinearLayout layout = findViewById(R.id.layout);
final Button scrollTo = findViewById(R.id.scroll_to);
/*
 * 第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动,
 * 第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动
 */
scrollTo.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //移动到相对于容器的左上角 x 200 y 200的一个位置
        //绝对坐标计算
        layout.scrollTo(-200, -200);
    }
});
  • scrollBy
final LinearLayout layout = findViewById(R.id.layout);
final Button scrollBy = findViewById(R.id.scroll_by);
/*
 * 第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动,
 * 第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动
 */
scrollBy.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //在相对于上次的位置移动50个px
        layout.scrollBy(-50, -50);
    }
});

接下来说Scroller这个类的使用,封装了滚动的操作

在布局中放置一个自定义的ViewGroup,并添加一个子View。
看下我们最终实现的效果,让View竖直向下滚动:

这里写图片描述

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.azhon.scroller.ScrollActivity">

    <com.azhon.scroller.MyViewGroup
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorAccent"
        android:gravity="center_horizontal"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent">

        <TextView
            android:id="@+id/tv"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@color/colorPrimary" />
    </com.azhon.scroller.MyView>

</android.support.constraint.ConstraintLayout>
  • 自定义的ViewGroup MyViewGroup
class MyViewGroup extends LinearLayout {

    private Scroller scroller;

    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        scroller = new Scroller(context);
    }

    public void startScroll() {
         /*
           调用startScroll()方法来初始化滚动数据并刷新界面
           startScroll()第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,
           第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动
          */
        scroller.startScroll(0, getScrollY(), 0, -200);
        invalidate();
    }

    @Override
    public void computeScroll() {
        //判断是否滚动完成
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
        }
    }
}
  • 通过点击TextView触发滚动逻辑MyViewGroup中的startScroll()
final MyViewGroup myView = findViewById(R.id.view);
final View tv = findViewById(R.id.tv);
tv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        myView.startScroll();
    }
  • 第一步:在ViewGroup的构造函数中初始化Scroller scroller = new Scroller(context);
  • 第二步:调用scroller.startScroll(0, getScrollY(), 0, -200); 设置View需要滚动到的位置,参数意思已经在上面代码中给出。这个函数只是对View滚动的一些参数设置,并没有开始滚动。
  • 第三步:调用View 的invalidate();对界面进行刷新,从而调用onDraw()函数而onDraw()函数中又调用了computeScroll()函数。
  • 第四步:重写computeScroll()函数对View进行滚动,一般情况下代码也就是上面这样固定的写法。scroller.getCurrX()、scroller.getCurrY()获取当前滚动的偏移量,

基础说完了,就可以自己动手写个案例练练手熟悉熟悉。

自定义ViewGroup 仿系统的ViewPage效果
自定义实现的效果:

这里写图片描述

  • 布局代码:摆放三个View
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.azhon.scroller.ScrollerViewActivity">

    <com.azhon.scroller.ScrollerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="true">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#24d278"
            android:gravity="center"
            android:text="第一页"
            android:textColor="#fff"
            android:textSize="20sp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#c124d2"
            android:gravity="center"
            android:text="第二页"
            android:textColor="#fff"
            android:textSize="20sp" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#97f21339"
            android:gravity="center"
            android:text="第三页"
            android:textColor="#fff"
            android:textSize="20sp" />
    </com.azhon.scroller.ScrollerView>
</android.support.constraint.ConstraintLayout>
  • 自定义Group的逻辑:宽度充满屏幕、里面的子View充满Group并水平排列View
  • 先处理自定Group逻辑,摆放子view 逻辑都写在注释里了
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = measure(widthMeasureSpec);

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            this.measureChild(child, widthMeasureSpec, heightMeasureSpec);
            // 获取容器内的 高度最高的一个View的高度 作为此Group的高度
            if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
                height = Math.max(height, child.getHeight());
            }
        }
        // 初始化右边界值 左边界为0
        rightBorder = getChildAt(getChildCount() - 1).getRight();
        //设置group的大小
        setMeasuredDimension(width, height);
    }

    private int measure(int spec) {
        DisplayMetrics dm = getResources().getDisplayMetrics();
        int defaultWidth = dm.widthPixels;
        switch (MeasureSpec.getMode(spec)) {
            //表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。
            // 这种情况比较少见,一般在绘制自定义View的时候才会用到
            case MeasureSpec.UNSPECIFIED:
                return defaultWidth;
            //wrap_content
            case MeasureSpec.AT_MOST:
                return defaultWidth;
            //match_parent 或者 固定大小的时候 layout_width="200dp"
            case MeasureSpec.EXACTLY:
                return MeasureSpec.getSize(spec);
            default:
                break;
        }
        return defaultWidth;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //所有View水平排列
        int childCount = getChildCount();
        int l1 = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            /*
              容器内view的总宽度
             */
            int totalWidth = l1 + childView.getMeasuredWidth();
            childView.layout(l1, 0, totalWidth, childView.getMeasuredHeight());
            l1 += childView.getMeasuredWidth();
        }
    }

接下来就是来处理我们的滑动逻辑,当往左滑动时候:下一页出现了1/3的宽度 那么就可以让它自动滚动到下一页,同理 当往右滑动的时候前一页出现了1/3 那么也让它自动滚动到前一页;当是第一页 或者 最后一页的时候就不允许滑动了。

  • 复写onTouchEvent 处理触摸事件
    /**
     * 当前容器的宽度
     */
    private int width;

    /**
     * 当前容器的高度
     */
    private int height;
    /**
     * 滚动
     */
    private Scroller scroller;
    /**
     * 最右边的边界
     */
    private int rightBorder;
    /**
     * x轴滑动的起点
     */
    private float lastX;

    /**
     * getRawX() 获取当前点距离屏幕的距离
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = event.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                float curr = event.getRawX();
                //向右滑动时为负数,计算与上次移动距离的差值
                float scrolled = lastX - curr;
                //getScrollX()获取x轴滑动的距离 向左滑动正值 反之负值
                if (getScrollX() + scrolled < 0) {
                    //当滑动到了最左边就不能继续滑动了
                    scrollTo(0, 0);
                    return true;
                } else if (getScrollX() + scrolled + width > rightBorder) {
                    //当滑动到最右边的时候就不能继续滑动了
                    scrollTo(rightBorder - width, 0);
                    return true;
                }
                scrollBy((int) scrolled, 0);
                lastX = curr;
                break;
            case MotionEvent.ACTION_UP:

                //需求:当第二个View进入屏幕1/3的时候 就让他自动滚动到第二个View上
                //往左滑动的距离加上 View的宽度(也就是屏幕宽度)的2/3 在除以单个View的宽度就可以知道
                //当前需要滑动到第几个View了
                int page = (getScrollX() + width * 2 / 3) / width;
                //  计算x轴需要滑动的距离
                int scrollX = page * width - getScrollX();

                /*
                    调用startScroll()方法来初始化滚动数据并刷新界面
                    startScroll()第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,
                    第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动
                    在滚动过程中会不断调用computeScroll()函数
                */
                scroller.startScroll(getScrollX(), 0, scrollX, 0);
                invalidate();
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
        }
    }

基本需要说的内容,都在代码的注释里了所以需要你仔细认真阅读代码 。
关于getRawX()的解释可以看这里自定义LinearLayout实现RatingBar效果,打造一个评分控件

源码在这里下载

阅读更多

扫码向博主提问

Code-Porter

博客专家

非学,无以致疑;非问,无以广识
  • 擅长领域:
  • Android
  • ReactNativ
  • 微信小程序
去开通我的Chat快问

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