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

2024-01-02 07:18

本文主要是介绍Android Scroller完全解析,关于Scroller你所需知道的一切,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/48719871
2016大家新年好!这是今年的第一篇文章,那么应CSDN工作人员的建议,为了能给大家带来更好的阅读体验,我也是将博客换成了宽屏版。另外,作为一个对新鲜事物从来后知后觉的人,我终于也在新的一年里改用MarkDown编辑器来写博客了,希望大家在我的博客里也能体验到新年新的气象。

我写博客的题材很多时候取决于平时大家问的问题,最近一段时间有不少朋友都问到ViewPager是怎么实现的。那ViewPager相信每个人都再熟悉不过了,因此它实在是太常用了,我们可以借助ViewPager来轻松完成页面之间的滑动切换效果,但是如果问到它是如何实现的话,我感觉大部分人还是比较陌生的, 为此我也是做了一番功课。其实说到ViewPager最基本的实现原理主要就是两部分内容,一个是事件分发,一个是Scroller,那么对于事件分发,其实我在很早之前就已经写过了相关的内容,感兴趣的朋友可以去阅读 Android事件分发机制完全解析,带你从源码的角度彻底理解,但是对于Scroller我还从来没有讲过,因此本篇文章我们就先来学习一下Scroller的用法,并结合事件分发和Scroller来实现一个简易版的ViewPager。


Scroller是一个专门用于处理滚动效果的工具类,可能在大多数情况下,我们直接使用Scroller的场景并不多,但是很多大家所熟知的控件在内部都是使用Scroller来实现的,如ViewPager、ListView等。而如果能够把Scroller的用法熟练掌握的话,我们自己也可以轻松实现出类似于ViewPager这样的功能。那么首先新建一个ScrollerTest项目,今天就让我们通过例子来学习一下吧。
先撇开Scroller类不谈,其实任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法,如下图所示:

这两个方法都是用于对View进行滚动的,那么它们之间有什么区别呢?简单点讲,scrollBy()方法是让View相对于当前的位置滚动某段距离,而scrollTo()方法则是让View相对于初始的位置滚动某段距离。这样讲大家理解起来可能有点费劲,我们来通过例子实验一下就知道了。

修改activity_main.xml中的布局文件,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/layout"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context="com.example.guolin.scrollertest.MainActivity"><Buttonandroid:id="@+id/scroll_to_btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="scrollTo"/><Buttonandroid:id="@+id/scroll_by_btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="scrollBy"/></LinearLayout>

外层我们使用了一个LinearLayout,然后在里面包含了两个按钮,一个用于触发scrollTo逻辑,一个用于触发scrollBy逻辑。

接着修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {private LinearLayout layout;private Button scrollToBtn;private Button scrollByBtn;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);layout = (LinearLayout) findViewById(R.id.layout);scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);scrollToBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {layout.scrollTo(-60, -100);}});scrollByBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {layout.scrollBy(-60, -100);}});}
}

没错,代码就是这么简单。当点击了scrollTo按钮时,我们调用了LinearLayout的scrollTo()方法,当点击了scrollBy按钮时,调用了LinearLayout的scrollBy()方法。那有的朋友可能会问了,为什么都是调用的LinearLayout中的scroll方法?这里一定要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,而LinearLayout中的内容就是我们的两个Button,如果你直接调用button的scroll方法的话,那结果一定不是你想看到的。

另外还有一点需要注意,就是两个scroll方法中传入的参数,第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动,单位是像素。第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动,单位是像素。

那说了这么多,scrollTo()和scrollBy()这两个方法到底有什么区别呢?其实运行一下代码我们就能立刻知道了:

可以看到,当我们点击scrollTo按钮时,两个按钮会一起向右下方滚动,因为我们传入的参数是-60和-100,因此向右下方移动是正确的。但是你会发现,之后再点击scrollTo按钮就没有任何作用了,界面不会再继续滚动,只有点击scrollBy按钮界面才会继续滚动,并且不停点击scrollBy按钮界面会一起滚动下去。

现在我们再来回头看一下这两个方法的区别,scrollTo()方法是让View相对于初始的位置滚动某段距离,由于View的初始位置是不变的,因此不管我们点击多少次scrollTo按钮滚动到的都将是同一个位置。而scrollBy()方法则是让View相对于当前的位置滚动某段距离,那每当我们点击一次scrollBy按钮,View的当前位置都进行了变动,因此不停点击会一直向右下方移动。

通过这个例子来理解,相信大家已经把scrollTo()和scrollBy()这两个方法的区别搞清楚了,但是现在还有一个问题,从上图中大家也能看得出来,目前使用这两个方法完成的滚动效果是跳跃式的,没有任何平滑滚动的效果。没错,只靠scrollTo()和scrollBy()这两个方法是很难完成ViewPager这样的效果的,因此我们还需要借助另外一个关键性的工具,也就我们今天的主角Scroller。

Scroller的基本用法其实还是比较简单的,主要可以分为以下几个步骤:

  1. 创建Scroller的实例
  2. 调用startScroll()方法来初始化滚动数据并刷新界面
  3. 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑

那么下面我们就按照上述的步骤,通过一个模仿ViewPager的简易例子来学习和理解一下Scroller的用法。
新建一个ScrollerLayout并让它继承自ViewGroup来作为我们的简易ViewPager布局,代码如下所示:

/*** Created by guolin on 16/1/12.*/
public class ScrollerLayout extends ViewGroup {/*** 用于完成滚动操作的实例*/private Scroller mScroller;/*** 判定为拖动的最小移动像素数*/private int mTouchSlop;/*** 手机按下时的屏幕坐标*/private float mXDown;/*** 手机当时所处的屏幕坐标*/private float mXMove;/*** 上次触发ACTION_MOVE事件时的屏幕坐标*/private float mXLastMove;/*** 界面可滚动的左边界*/private int leftBorder;/*** 界面可滚动的右边界*/private int rightBorder;public ScrollerLayout(Context context, AttributeSet attrs) {super(context, attrs);// 第一步,创建Scroller的实例mScroller = new Scroller(context);ViewConfiguration configuration = ViewConfiguration.get(context);// 获取TouchSlop值mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int childCount = getChildCount();for (int i = 0; i < childCount; i++) {View childView = getChildAt(i);// 为ScrollerLayout中的每一个子控件测量大小measureChild(childView, widthMeasureSpec, heightMeasureSpec);}}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {if (changed) {int childCount = getChildCount();for (int i = 0; i < childCount; i++) {View childView = getChildAt(i);// 为ScrollerLayout中的每一个子控件在水平方向上进行布局childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());}// 初始化左右边界值leftBorder = getChildAt(0).getLeft();rightBorder = getChildAt(getChildCount() - 1).getRight();}}@Overridepublic 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;// 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件if (diff > mTouchSlop) {return true;}break;}return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_MOVE:mXMove = event.getRawX();int scrolledX = (int) (mXLastMove - mXMove);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();// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面mScroller.startScroll(getScrollX(), 0, dx, 0);invalidate();break;}return super.onTouchEvent(event);}@Overridepublic void computeScroll() {// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑if (mScroller.computeScrollOffset()) {scrollTo(mScroller.getCurrX(), mScroller.getCurrY());invalidate();}}
}

整个Scroller用法的代码都在这里了,代码并不长,一共才100多行,我们一点点来看。
首先在ScrollerLayout的构造函数里面我们进行了上述步骤中的第一步操作,即创建Scroller的实例,由于Scroller的实例只需创建一次,因此我们把它放到构造函数里面执行。另外在构建函数中我们还初始化的TouchSlop的值,这个值在后面将用于判断当前用户的操作是否是拖动。

接着重写onMeasure()方法和onLayout()方法,在onMeasure()方法中测量ScrollerLayout里的每一个子控件的大小,在onLayout()方法中为ScrollerLayout里的每一个子控件在水平方向上进行布局。如果有朋友对这两个方法的作用还不理解,可以参照我之前写的一篇文章 Android视图绘制流程完全解析,带你一步步深入了解View(二) 。

接着重写onInterceptTouchEvent()方法, 在这个方法中我们记录了用户手指按下时的X坐标位置,以及用户手指在屏幕上拖动时的X坐标位置,当两者之间的距离大于TouchSlop值时,就认为用户正在拖动布局,然后我们就将事件在这里拦截掉,阻止事件传递到子控件当中。

那么当我们把事件拦截掉之后,就会将事件交给ScrollerLayout的onTouchEvent()方法来处理。如果当前事件是ACTION_MOVE,说明用户正在拖动布局,那么我们就应该对布局内容进行滚动从而影响拖动事件,实现的方式就是使用我们刚刚所学的scrollBy()方法,用户拖动了多少这里就scrollBy多少。另外为了防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用scrollTo()方法来回到边界位置。

如果当前事件是ACTION_UP时,说明用户手指抬起来了,但是目前很有可能用户只是将布局拖动到了中间,我们不可能让布局就这么停留在中间的位置,因此接下来就需要借助Scroller来完成后续的滚动操作。首先这里我们先根据当前的滚动位置来计算布局应该继续滚动到哪一个子控件的页面,然后计算出距离该页面还需滚动多少距离。接下来我们就该进行上述步骤中的第二步操作,调用startScroll()方法来初始化滚动数据并刷新界面。startScroll()方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。

现在前两步都已经完成了,最后我们还需要进行第三步操作,即重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的curX和curY坐标传入,然后刷新界面从而完成平滑滚动的操作。

现在ScrollerLayout已经准备好了,接下来我们修改activity_main.xml布局中的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<com.example.guolin.scrollertest.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><Buttonandroid:layout_width="match_parent"android:layout_height="100dp"android:text="This is first child view"/><Buttonandroid:layout_width="match_parent"android:layout_height="100dp"android:text="This is second child view"/><Buttonandroid:layout_width="match_parent"android:layout_height="100dp"android:text="This is third child view"/></com.example.guolin.scrollertest.ScrollerLayout>

可以看到,这里我们在ScrollerLayout中放置了三个按钮用来进行测试,其实这里不仅可以放置按钮,放置任何控件都是没问题的。

最后MainActivity当中删除掉之前测试的代码:

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}
}

好的,所有代码都在这里了,现在我们可以运行一下程序来看一看效果了,如下图所示:

怎么样,是不是感觉有点像一个简易的ViewPager了?其实借助Scroller,很多漂亮的滚动效果都可以轻松完成,比如实现图片轮播之类的特效。当然就目前这一个例子来讲,我们只是借助它来学习了一下Scroller的基本用法,例子本身有很多的功能点都没有去实现,比如说ViewPager会根据用户手指滑动速度的快慢来决定是否要翻页,这个功能在我们的例子中并没有体现出来,不过大家也可以当成自我训练来尝试实现一下。


好的,那么本篇文章就到这里,相信通过这篇文章的学习,大家已经能够熟练掌握Scroller的使用方法了,当然ViewPager的内部实现要比这复杂得多,如果有朋友对ViewPager的源码感兴趣也可以尝试去读一下,不过一定需要非常扎实的基本功才行。

关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

微信扫一扫下方二维码即可关注:

        

这篇关于Android Scroller完全解析,关于Scroller你所需知道的一切的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/561700

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

Android里面的Service种类以及启动方式

《Android里面的Service种类以及启动方式》Android中的Service分为前台服务和后台服务,前台服务需要亮身份牌并显示通知,后台服务则有启动方式选择,包括startService和b... 目录一句话总结:一、Service 的两种类型:1. 前台服务(必须亮身份牌)2. 后台服务(偷偷干

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

MySQL 缓存机制与架构解析(最新推荐)

《MySQL缓存机制与架构解析(最新推荐)》本文详细介绍了MySQL的缓存机制和整体架构,包括一级缓存(InnoDBBufferPool)和二级缓存(QueryCache),文章还探讨了SQL... 目录一、mysql缓存机制概述二、MySQL整体架构三、SQL查询执行全流程四、MySQL 8.0为何移除查

在Rust中要用Struct和Enum组织数据的原因解析

《在Rust中要用Struct和Enum组织数据的原因解析》在Rust中,Struct和Enum是组织数据的核心工具,Struct用于将相关字段封装为单一实体,便于管理和扩展,Enum用于明确定义所有... 目录为什么在Rust中要用Struct和Enum组织数据?一、使用struct组织数据:将相关字段绑

使用Java实现一个解析CURL脚本小工具

《使用Java实现一个解析CURL脚本小工具》文章介绍了如何使用Java实现一个解析CURL脚本的工具,该工具可以将CURL脚本中的Header解析为KVMap结构,获取URL路径、请求类型,解析UR... 目录使用示例实现原理具体实现CurlParserUtilCurlEntityICurlHandler

深入解析Spring TransactionTemplate 高级用法(示例代码)

《深入解析SpringTransactionTemplate高级用法(示例代码)》TransactionTemplate是Spring框架中一个强大的工具,它允许开发者以编程方式控制事务,通过... 目录1. TransactionTemplate 的核心概念2. 核心接口和类3. TransactionT

数据库使用之union、union all、各种join的用法区别解析

《数据库使用之union、unionall、各种join的用法区别解析》:本文主要介绍SQL中的Union和UnionAll的区别,包括去重与否以及使用时的注意事项,还详细解释了Join关键字,... 目录一、Union 和Union All1、区别:2、注意点:3、具体举例二、Join关键字的区别&php

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤

Spring IOC控制反转的实现解析

《SpringIOC控制反转的实现解析》:本文主要介绍SpringIOC控制反转的实现,IOC是Spring的核心思想之一,它通过将对象的创建、依赖注入和生命周期管理交给容器来实现解耦,使开发者... 目录1. IOC的基本概念1.1 什么是IOC1.2 IOC与DI的关系2. IOC的设计目标3. IOC