滑动是Android开发中非常重要的UI效果,几乎所有应用都包含了滑动效果,而本文将对滑动的使用以及原理进行介绍。

一、scrollTo与ScrollBy

View提供了专门的方法用于实现滑动效果,分别为scrollTo与scrollBy。先来看看它们的源码:

/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
} /**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}

从源码中可以看出scrollBy实际上是调用了scrollTo函数来实现它的功能。scrollBy实现的是输入参数的相对滑动,而scrollTo是绝对滑动。需要说明的是mScrollX、mScrollY这两个View的属性,这两个属性可以通过getScrollX、getScrollY获得。

  • mScrollX : View的左边缘与View内容的左边缘在水平方向上的距离,即从右向左滑动时,为正值,反之为负值。
  • mScrollY : View的上边缘与View内容的上边缘在竖直方向上的距离,即从下向上滑动时,为正值,反之为负值。
  • 下面我们来实现一个滑动的效果:
public class HorizontalScroller extends ViewGroup {

    private int mTouchSlop;

    private float mLastXIntercept=0;
private float mLastYIntercept=0; private float mLastX=0;
private float mLastY=0; private int leftBorder;
private int rightBorder; public HorizontalScroller(Context context) {
super(context);
init(context);
} public HorizontalScroller(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
} public HorizontalScroller(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
} private void init(Context context){
ViewConfiguration configuration = ViewConfiguration.get(context);
// 获取TouchSlop值
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
} @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
float xIntercept = ev.getX();
float yIntercept = ev.getY(); switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = xIntercept-mLastXIntercept;
float deltaY = yIntercept-mLastYIntercept;
// 当水平方向的滑动距离大于竖直方向的滑动距离,且手指拖动值大于TouchSlop值时,拦截事件
if (Math.abs(deltaX)>Math.abs(deltaY) && Math.abs(deltaX)>mTouchSlop) {
intercept=true;
}else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
break;
} mLastX = xIntercept;
mLastY = yIntercept;
mLastXIntercept = xIntercept;
mLastYIntercept = yIntercept; return intercept;
} @Override
public boolean onTouchEvent(MotionEvent event) {
float xTouch = event.getX();
float yTouch = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
float deltaX = xTouch-mLastX;
float deltaY = yTouch-mLastY;
float scrollByStart = deltaX;
if (getScrollX() - deltaX < leftBorder) {
scrollByStart = getScrollX()-leftBorder;
} else if (getScrollX() + getWidth() - deltaX > rightBorder) {
scrollByStart = rightBorder-getWidth()-getScrollX();
}
scrollBy((int) -scrollByStart, 0);
break;
case MotionEvent.ACTION_UP:
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
scrollTo(getScrollX()+dx,0);
break;
default:
break;
} mLastX = xTouch;
mLastY = yTouch; return super.onTouchEvent(event);
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 测量每一个子控件的大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} @Override
protected 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);
// 在水平方向上对子控件进行布局
childView.layout(i * getMeasuredWidth(), 0, i * getMeasuredWidth()+childView.getMeasuredWidth()+getPaddingLeft(), childView.getMeasuredHeight());
}
// 初始化左右边界值
leftBorder = 0;
rightBorder = getChildCount()*getMeasuredWidth();
}
}
}

现在我们来分析下这段代码:

  • 首先在构造函数中获取了最小滑动距离TouchSlop。
  • 重写onInterceptTouchEvent拦截事件,记录当前坐标。点下时,默认不拦截,只有当滑动还未完成的情况下,才继续拦截。在移动时,对滑动冲突进行了处理,当水平方向的移动距离大于竖直方向的移动距离,并且移动距离大于最小滑动距离时,我们判断此时为水平滑动,拦截事件自己处理;否则不拦截,交由子View处理。提起手指时,同样不拦截事件。
  • 重写onTouchEvent处理事件,记录当前坐标。在手指按下时,与拦截事件时做相似处理。在ACTION_MOVE时,向左滑动,如果滑动距离超过左边界,则对滑动距离进行处理,相对的滑动距离超出又边界,也是一样处理,之后把滑动的距离交给scrollBy进行处理。当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面,然后使用scrollTo滑动到那个子控件。
  • 重写了onMeasure和onLayout方法,在onMeasure中测量每一个子控件的大小值,在onLayout中对每一个子view在水平方向上进行布局。子view的layout的right增加父类的paddingLeft参数,来处理设置padding的情况。这两个函数的流程分析将会放在之后的文章中详细说明。

这个类的使用方法如下 :

<?xml version="1.0" encoding="utf-8"?>
<com.idtk.customscroll.HorizontalScroller
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
tools:context="com.idtk.customscroll.MainActivity"> <ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/zhiqinchun"
android:clickable="true"/> <ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/hanzhan"
android:clickable="true"/> <ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/shengui"
android:clickable="true"/> <ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/dayu"
android:clickable="true"/> </com.idtk.customscroll.HorizontalScroller>

HorizontalScroller设置全屏,padding为10dp。使用4个ImageView作为子View,并且都设置为可点击状态。示例效果图如下:

二、Scroller

可以看到上面使用scrollTo与ScrollBy方法的滑动都是瞬时完成的,这有些无法满足我们在切换子view时的需求。我们希望切换子View时,可以拥有滑动过程的效果,而Scroller正好可以完成这一点。

Scroller的使用方法:

* 1、创建Scroller实例

* 2、使用startScroll方法,对其进行初始化

* 3、重写computeScroll()方法,在其内部调用scrollTo或ScrollBy方法,完成滑动过程。

//创建实例
mScroller = new Scroller(context); public void smoothScrollTo(){
int ScrollX = getScrollX();
int ScrollY = getScrollY();
//初始化,1000ms内缓慢滑动到deltaX
mScroller.startScroll(ScrollX, 0, 0, deltaX, 1000);
invalidate();
} @Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
scrollTo(currX, currY);
postInvalidate();
}
}

上面的代码是Scroller的典型用法,也就是传说中的套路。当时Scroller使用startScroll方法时,只是对一系列参数进行了初始化。我们从下面的源码中可以看出。

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}

参数中,startX、startY是滑动的起点,dx、dy是滑动的距离,duration是滑动的时间系统设置为250ms。我们可以看到startScroll只是进行了滑动时间、是否滑动完成、起点、终点、滑动距离等的参数的设置,那么是如何调用computeScroll()函数的呢?其实computeScroll()的调用是由之后的invalidate()函数来完成的,invalidate可以请求View重绘,在View重绘时会调用draw方法,draw方法又会去调用computeScroll函数。但computeScroll()函数在view中是一个空的函数,需要我们去实现它。

computeScroll()函数的实现已经在上面给出了,有了computeScroll方法之后,就可以实现View的弹性滑动了。来看下computeScroll()的实现过程,首先要进行computeScrollOffset()的判断,来看下它的源码 :

public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX); mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}

computeScrollOffset()首先检测scroller是否完成滑动,完成则返回false,未完成则继续AnimationUtils.currentAnimationTimeMillis获取当前的毫秒值,减去之前startScroll方法时获得毫秒值,就是当前滑动的执行时间。之后判断执行时间是否小于设置的总时间,如果小于,根据startScroll时设置的模式SCROLL_MODE,然后根据Interpolator计算出当前滑动的mcurrX、mcurrY(顺便提一下在实例化scroller的时候,是可以设置动画插值器。);如果执行时间大于或者等于设置的总时间,则直接设置mcurrX、mcurrY为终点值,并且设置mFinished,表示动画已经完成。

Scroller弹性滑动的流程如下

现在使用Scroller方法来更改一下上面的代码,当ACTION_UP时,子View的滑动可以有一个过程,而不是瞬时完成。

    private Scroller mScroller;
...
private void init(Context context){
// 第一步,创建Scroller的实例
mScroller = new Scroller(context);
...
} @Override
public boolean onTouchEvent(MotionEvent event) {
float xTouch = event.getX();
float yTouch = event.getY();
switch (event.getAction()) {
...
case MotionEvent.ACTION_UP:
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
// 第二步,使用startScroll方法,对其进行初始化
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
default:
break;
} mLastX = xTouch;
mLastY = yTouch; return super.onTouchEvent(event);
}
...
@Override
public void computeScroll() {
// 第三步,重写computeScroll()方法,在其内部调用scrollTo或ScrollBy方法,完成滑动过程
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
}

上面就是代码中需要增加和修改的部分,我们来简单分析下。

  • 在构造函数中增加对 Scroller进行了实例化 。
  • 替换onTouchEvent中手指抬起后的方法,改为 使用startScroll方法,对mScroller进行初始化 ,之后invalidate请求重绘。
  • 增加 重写的computeScroll()方法 ,在其内部调用scrollTo或ScrollBy方法,完成滑动过程,之后使用postInvalidate()请求view重绘。

示例效果图如下 :

三、回弹效果

从上面的效果图可以看出,我们已经实现了view的平滑滚动,滑动位置超过当前view的1/2时,松手之后变会自动滑出此item的View。可是如果想要在首位两端实现回弹效果,该如何做呢?其实只要修改onTouchEvent方法即可。

@Override
public boolean onTouchEvent(MotionEvent event) {
float xTouch = event.getX();
float yTouch = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished())
mScroller.abortAnimation();
break;
case MotionEvent.ACTION_MOVE:
float deltaX = xTouch-mLastX;
float deltaY = yTouch-mLastY;
float scrollByStart = deltaX;
//如果超出边界,则把滑动距离缩小到1/3
if (getScrollX() - deltaX < leftBorder) {
scrollByStart = deltaX/3;
} else if (getScrollX() + getWidth() - deltaX > rightBorder) {
scrollByStart = deltaX/3;
}
scrollBy((int) -scrollByStart, 0);
break;
case MotionEvent.ACTION_UP:
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
//如果超过右边界,则回弹到最后一个View
if (targetIndex>getChildCount()-1){
targetIndex = getChildCount()-1;
//如果超过左边界,则回弹到第一个View
}else if (targetIndex<0){
targetIndex =0;
}
int dx = targetIndex * getWidth() - getScrollX();
// 第二步,使用startScroll方法,对其进行初始化
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
default:
break;
}
mLastX = xTouch;
mLastY = yTouch;
return super.onTouchEvent(event);
}

来简单分析下修改的onTouchEvent方法:

在滑动的过程中,如果滑动的位置超过了试图的左、右边界,则缩小View的滑动距离,使之为手指滑动距离的1/3。当手指离开时,如果通过view宽度获得的当前inder小与0,则index为第一个View;如果获得的当前index超过了子View的数量-1,则index为最后一个View。View的回弹效果如下:

四、小结

本文介绍弹性滑动的实现方法,并对弹性滑动的过程进行了详细分析。在之后通过例子实现了view的弹性滑动以及回弹效果,但 最后还留有两个问题,即invalidate与postInvalidate的区别又在哪里呢?invalidate是如何调用computeScroll()函数的呢? ,这些问题我将在下一篇文章中进行详细的分析。

来自:http://www.idtkm.com/customview/customview8/

扩展阅读

iOS开发者必备:自己总结的iOS、mac开源项目及库
Android开发之自定义View
Android自定义View系列(一)——打造一个爱心进度条
android中正确保存view的状态
iOS、mac开源项目及库汇总

为您推荐

Android自定义下拉刷新动画--仿百度外卖下拉刷新
Android Activity悬浮并可拖动(访悬浮歌词)
Android SwipeBackLayout源码解析
ScratchView:一步步打造万能的 Android 刮奖效果控件
自定义View系列教程01--常用工具介绍

更多

安卓开发
Android
View
Android开发

相关文档  — 更多
相关经验  — 更多
相关讨论  — 更多

(转载)自定义View——弹性滑动的更多相关文章

  1. 【朝花夕拾】Android自定义View篇之(十一)View的滑动,弹性滑动与自定义PagerView

    前言 由于手机屏幕尺寸有限,但是又经常需要在屏幕中显示大量的内容,这就使得必须有部分内容显示,部分内容隐藏.这就需要用一个Android中很重要的概念——滑动.滑动,顾名思义就是view从一个地方移动 ...

  2. 安卓自定义View进阶-Canvas之画布操作 转载

    安卓自定义View进阶-Canvas之画布操作 转载 https://www.gcssloop.com/customview/Canvas_Convert 本来想把画布操作放到后面部分的,但是发现很多 ...

  3. 分析自定义view的实现过程-实现雪花飞舞效果(转载有改动)

    声明:本文源码出自实现雪花飞舞效果(有改动)主要通过这篇文来分析自定义view的实现过程. 没事时,比较喜欢上网看看一些新的东西,泡在网上的日子就是一个很不错的网站. 下面开始了,哈哈.^_^ 大家都 ...

  4. 转载:【译】Android: 自定义View

    简介 每天我们都会使用很多的应用程序,尽管他们有不同的约定,但大多数应用的设计是非常相似的.这就是为什么许多客户要求使用一些其他应用程序没有的设计,使得应用程序显得独特和不同. 如果功能布局要求非常定 ...

  5. 【转载】自定义View,有这一篇就够了

    为了扫除学习中的忙点,尽可能多的覆盖Android知识的边边角角,决定对自定义View做一个稍微全面一点的使用方法总结,在内容上面并没有什么独特的地方,其他大神们博客上面基本上都有讲这方面的内容,如果 ...

  6. 【转载】自定义View学习笔记之详解onMeasure

    网上对自定义View总结的文章都很多,但是自己还是写一篇,好记性不如多敲字! 其实自定义View就是三大流程,onMeasure.onLayout.onDraw.看名字就知道,onMeasure是用来 ...

  7. 【转载】深入剖析自定义View之onMeasure

    1.前言 自定义View中我们看到很多都重写了onMeasure方法,那么我们首先得知道onMeasure是做什么的.onMeasure中文意思就是测量,所以它是用于测量View的大小,影响View大 ...

  8. Android 自定义View及其在布局文件中的使用示例

    前言: 尽管Android已经为我们提供了一套丰富的控件,如:Button,ImageView,TextView,EditText等众多控件,但是,有时候在项目开发过程中,还是需要开发者自定义一些需要 ...

  9. Android自定义View之CircleView

    Android自定义View之CircleView 版权声明:本文为博主原创文章,未经博主允许不得转载. 转载请表明出处:http://www.cnblogs.com/cavalier-/p/5999 ...

随机推荐

  1. POJ 3020 Hungary

    一道建图题-- // by SiriusRen #include <cstdio> #include <cstring> using namespace std; #defin ...

  2. linux的chmod,chown命令 详解

    指令名称 : chmod 使用权限 : 所有使用者 使用方式 : chmod [-cfvR] [--help] [--version] mode file... 说明 : Linux/Unix 的档案 ...

  3. Spring《二》 Bean的生命周期

    Bean初始化 1.bean中实现public void init():方法,config.xml中增加init-method="init" 属性. 2.bean实现接口Initi ...

  4. DB2导出表结构、表数据小结

    一.DB2命令行导出数据库全库表结构 ① Win+R进入到DB2安装目录的BIN目录下,执行命令:DB2CMD,进入到DB2 CLP窗口. 命令:DB2CMD ② 创建一个data文件夹 命令:MKD ...

  5. 常规RPC通讯过程【转载】

    在 HTTP2 协议正式开始工作前, 如果已经知道服务器是 HTTP2 的服务器, 通讯流程如下: 客户端必须首先发送一个连接序言,其逻辑结构: PRI * HTTP/2.0\r\n\r\nSM\r\ ...

  6. C++函数的高级特性——小结

    相对于C语言,C++增加了重载(overload).内联(inline).const和virtual四种新机制. 1 重载 只能靠参数列表而不能紧靠返回值类型的不同来区分重载函数.编译器根据参数列表为 ...

  7. 12、Camel: Content-Aware and Meta-path Augmented Metric Learning for Author Identification----作者识别

    摘自:https://blog.csdn.net/me_yundou/article/details/80459341 具体看上面链接 一.摘要: 这篇文章主要介绍的是作者识别(author iden ...

  8. 编写高性能的javascript代码(持续更新)

    参考资料: Vanilla JS——世界上最轻量的JavaScript框架(没有之一) http://segmentfault.com/a/1190000000355277 探索高效jQuery的奥秘 ...

  9. git指令总结及常见问题积累与解决方案

    git指令总结及常见问题积累与解决方案 git初始化一个项目并且长传到服务器后端步骤: 1.本地文件操作 通过:git init初始化化一个项目  会出现一个隐藏文件 ,可以文件夹属性设置进行查看,此 ...

  10. HDU 2150 Pipe( 判断线段相交水 )

    链接:传送门 题意:略 思路:数据量很小,直接暴力所有线段 /********************************************************************* ...