【本文出自大圣代的技术专栏 http://blog.csdn.net/qq_23191031】
【禁止任何商业活动。转载烦请注明出处】
学前准备
详解Android控件体系与常用坐标系
Android常用触控类分析:MotionEvent 、 ViewConfiguration、VelocityTracker
前言
在前面的几篇文章,我向大家介绍的都是单一View事件,从这篇文章开始,我将向大家介绍连续的事件 —— 滑动。滑动是移动端设备提供的重要功能,正是由于强大的滑动事件让我们小巧的屏幕可以展现无限的数据。而滑动事件冲突却常常困扰着广大开发者。孙子云:知己知彼,百战不殆。想更好的协调滑动事件,不知道其中原理的确困难重重。当你学习本篇文章之后你会发现其实Scroll很简单,你只是被各种文章与图书弄糊涂了。
在真正讲解之前,我们需要掌握Android坐标系与触控事件相关知识,对此不太明确的同学请参见上文的 学前准备
View滑动产生的原理
从原理上讲View滑动的本质就是随着手指的运动不断地改变坐标。当触摸事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后的触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标,不断的重复这样的过程,从而实现滑动过程。
1 scrollTo 与 scrollBy
说到Scroll就不得不提到scrollTo()与scrollBy()这两个方法。
1.1 scrollTo
首先我们要知道Android每一个控件都有滚动条,只不过系统对我们隐藏了,所以我们看不见。
对于控件来说它的大小是有限的,(例如我们指定了大小、屏幕尺寸的束缚等),系统在绘制图像的时候只会在这个有限的控件内绘制,但是内容(content)的载体Canvas在本质上是无限的,例如我们的开篇图片,控件仿佛就是一个窗口我们只能通过它看到这块画布。
/**
* 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; // 已经滚动到的X
int oldY = mScrollY; //已经滚动到的Y
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);//回调方法,通知状态改变
if (!awakenScrollBars()) {
postInvalidateOnAnimation(); //重新绘制
}
}
}
通过注释Set the scrolled position of your view
我们可以清楚的得知 scrollTo(x,y)的作用就是将View滚动到(x,y)这个点,注意是滚动(scroll本意滚动,滑动是translate)。
在初始时 mScrollX 与mScrollY均为0,表示着View中展示的是从画布左上角开始的内容(如图 1),当调用scrollTo(100,100)时相当于将View的坐标原点滚动到(100,100)这个位置,展示画布上从(100,100)开始的内容(如图2),但是事实上View是静止不动的,所以最终的效果是View的内容平移了(-100,-100)的偏移量(如图3)
1.2 scrollBy
/**
* 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);
}
学习scrollTo在学习scrollBy就简单了,通过源码可以看到它里面调用了ScrollTo(),传入的参数是mScrollX+x,也就是说这次x是一个增量,所以scrollBy实现的效果就是,在当前位置上,再偏移x距离
这是ScrollTo()和ScrollBy()的重要区别。
1.3 小结:
- scrollTo与scrollBy都会另View立即重绘,所以移动是瞬间发生的
- scrollTo(x,y):指哪打哪,效果为View的左上角滚动到(x,y)位置,但由于View相对与父View是静止的所以最终转换为相对的View的内容滑动到(-x,-y)的位置。
- scrollBy(x,y): 此时的x,y为偏移量,既在原有的基础上再次滚动
- scrollTo与scrollBy的最用效果会作用到View的内容,所以要是想滑动当前View,就需要对其父View调用二者。也可以在当前View中使用
((View)getParent).scrollXX(x,y)
达到同样目的。
2 Scroller
OK,通过上面的学习我们知道scrollTo与scrollBy可以实现滑动的效果,但是滑动的效果都是瞬间完成的,在事件执行的时候平移就已经完成了,这样的效果会让人感觉突兀,Google建议使用自然过渡的动画来实现移动效果。因此,Scroller类这样应运而生了。
2.1 简单实例
举一个简单的实例方便大家的理解与学习 Scroller
主要代码
public class CustomScrollerView extends LinearLayout {
private Scroller mScroller;
private View mLeftView;
private View mRightView;
private float mInitX, mInitY;
private float mOffsetX, mOffsetY;
public CustomScrollerView(Context context) {
this(context, null);
}
public CustomScrollerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
this.setOrientation(LinearLayout.HORIZONTAL);
mScroller = new Scroller(getContext(), null, true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 2) {
throw new RuntimeException("Only need two child view! Please check you xml file!");
}
mLeftView = getChildAt(0);
mRightView = getChildAt(1);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitX = ev.getX();
mInitY = ev.getY();
super.dispatchTouchEvent(ev);
return true;
case MotionEvent.ACTION_MOVE:
//>0为手势向右下
mOffsetX = ev.getX() - mInitX;
mOffsetY = ev.getY() - mInitY;
//横向手势跟随移动
if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) {
int offset = (int) -mOffsetX;
if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) {
return true;
}
this.scrollBy(offset, 0);
mInitX = ev.getX();
mInitY = ev.getY();
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//松手时刻滑动
int offset = ((getScrollX() / (float) mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;
// this.scrollTo(offset, 0);
mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset - this.getScrollX(), 0);
invalidate();
mInitX = 0;
mInitY = 0;
mOffsetX = 0;
mOffsetY = 0;
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate(); //允许在非主线程中出发重绘,它的出现就是简化我们在非UI线程更新view的步骤
}
}
}
主要布局
通过上面实例我们可以发现在自定义View的过程中使用Scroller的流程如下图所示:
下面我们就按照这个流程进行源码分析吧
2.2 源码分析
对于Scroller类 Google给出的如下解释:
This class encapsulates scrolling. You can use scrollers ( Scroller or OverScroller) to collect the data you need to produce a scrolling animation
for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don't automatically apply those positions to your view. It's your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.
我们中可以看出:Scroller 是一个工具类,它只是产生一些坐标数据,而真正让View平滑的滚动起来还需要我们自行处理。我们使用的处理工具就是—— scrollTo与scrollBy
2.2.1 构造方法分析
public Scroller(Context context) {
this(context, null);
}
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
mFinished = true;
if (interpolator == null) {
mInterpolator = new ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
//摩擦力计算单位时间减速度
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mFlywheel = flywheel;
mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}
Scroller的构造方法没啥特殊的地方只不过第二个参数interpolator是插值器,不同的插值器实现不同的动画算法(这里不是重点不做展开,以后重点讲解),如果我们不传,则默认使用ViscousFluidInterpolator()
插值器。
2.2.2 startScroll与fling
/**
* 使用默认滑动时间完成滑动
*/
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
/**
* 在我们想要滚动的地方调运,准备开始滚动,手动设置滚动时间
*
* @param startX 滑动起始X坐标
* @param startY 滑动起始Y坐标
* @param dx X方向滑动距离
* @param dy Y方向滑动距离
* @param duration 完成滑动所需的时间
*/
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;
}
/**
* 开始基于滑动手势的滑动。根据初始的滑动手势速度,决定滑动的距离(滑动的距离,不能大于设定的最大值,不能小于设定的最小值)
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
......
mMode = FLING_MODE;
mFinished = false;
......
mStartX = startX;
mStartY = startY;
......
mDistance = (int) (totalDistance * Math.signum(velocity));
mMinX = minX;
mMaxX = maxX;
mMinY = minY;
mMaxY = maxY;
......
mFinalY = Math.min(mFinalY, mMaxY);
mFinalY = Math.max(mFinalY, mMinY);
}
在这两个方法中,都是一些全局变量的赋值,果真没有实现滚动的方法,也佐证了Scroller是一个工具的解读。而要实现滑动还是要依靠我们手动调用View的invalidated()
方法触发computeScroll()
方法。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate(); //允许在非主线程中出发重绘,它的出现就是简化我们在非UI线程更新view的步骤
}
}
一旦触发成功就会调用Scroller.computeScrollOffset()
方法,返回结果如果为true表示当前的滑动尚未结束,如果返回false表示滑动完成。
在Scroller类中,最最重要的就是这个computeScrollOffset
方法,看上去只是返回了一个boolean
类型,但他却是Scroller的核心,所有的坐标与滑动时间都由它计算完成。他将原本瞬间的滑动拆分成连续平滑的过程。
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished. loc will be altered to provide the
* new location.
* 调用这个函数获得新的位置坐标(滑动过程中)。如果它返回true,说明滑动没有结束。
* getCurX(),getCurY()方法就可以获得计算后的值。
*/
public boolean computeScrollOffset() {
if (mFinished) {//是否结束
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);//滑动开始,经过了多长时间
if (timePassed < mDuration) {//如果经过的时间小于动画完成所需时间
switch (mMode) {
case SCROLL_MODE:
float x = timePassed * mDurationReciprocal;
if (mInterpolator == null)//如果没有设置插值器,利用默认算法
x = viscousFluid(x);
else//否则利用插值器定义的算法
x = mInterpolator.getInterpolation(x);
mCurrX = mStartX + Math.round(x * mDeltaX);//计算当前X坐标
mCurrY = mStartY + Math.round(x * mDeltaY);//计算当前Y坐标
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE[index];
final float d_sup = SPLINE[index + 1];
final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);
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;
}
从代码可以看到,如果我们没有设置插值器,就会调用内部默认算法。
/**
* 函数翻译是粘性流体
* 估计是一种算法
*/
static float viscousFluid(float x)
{
x *= sViscousFluidScale;
if (x < 1.0f) {
x -= (1.0f - (float)Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float)Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
x *= sViscousFluidNormalize;
return x;
}
接着是两个重要的get方法
/**
* Returns the current X offset in the scroll.
*
* @return The new X offset as an absolute distance from the origin.
* 获得当前X方向偏移
*/
public final int getCurrX() {
return mCurrX;
}
/**
* Returns the current Y offset in the scroll.
*
* @return The new Y offset as an absolute distance from the origin.
* 获得当前Y方向偏移
*/
public final int getCurrY() {
return mCurrY;
}
2.2.3 其他方法
public class Scroller {
......
public Scroller(Context context) {}
public Scroller(Context context, Interpolator interpolator) {}
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {}
//设置滚动持续时间
public final void setFriction(float friction) {}
//返回滚动是否结束
public final boolean isFinished() {}
//强制终止滚动
public final void forceFinished(boolean finished) {}
//返回滚动持续时间
public final int getDuration() {}
//返回当前滚动的偏移量
public final int getCurrX() {}
public final int getCurrY() {}
//返回当前的速度
public float getCurrVelocity() {}
//返回滚动起始点偏移量
public final int getStartX() {}
public final int getStartY() {}
//返回滚动结束偏移量
public final int getFinalX() {}
public final int getFinalY() {}
//实时调用该方法获取坐标及判断滑动是否结束,返回true动画没结束
public boolean computeScrollOffset() {}
//滑动到指定位置
public void startScroll(int startX, int startY, int dx, int dy) {}
public void startScroll(int startX, int startY, int dx, int dy, int duration) {}
//快速滑动松开手势惯性滑动
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {}
//终止动画,滚到最终的x、y位置
public void abortAnimation() {}
//延长滚动的时间
public void extendDuration(int extend) {}
//返回滚动开始经过的时间
public int timePassed() {}
//设置终止时偏移量
public void setFinalX(int newX) {}
public void setFinalY(int newY) {}
}
3 总结:
- 滑动的本质就是View随着手指的运动不断地改变坐标
- scrollTo(x,y)指的就是
View
滚动到(x,y)这个位置,但是View 要相当于父控件静止不懂,所以相对的View的内容
就会滑动到(-x, -y)的位置 - scrollTo、scrollBy移动是瞬间的
- 滑动效果作用的对象是View内容
- Scroller类其实是一个工具类,生产滑动过程的平滑坐标,但最终的滑动动作还是需要我们自行处理
- Scroller类的使用流程:
参考
《Android群英传》
http://blog.csdn.net/crazy__chen/article/details/45896961
http://blog.csdn.net/yanbober/article/details/49904715
版权声明:
禁止一切商业行为,转载请著名出处 http://blog.csdn.net/qq_23191031。作者: 大圣代
Copyright (c) 2017 代圣达. All rights reserved.