View的事件体系之二 View的滑动以及弹性滑动
新年第一更,之前也有看过View体系系列文章,内容有点生疏了,重新温习一下,基础篇已经整理过了,接下来会重新梳理一遍关于View的整个体系的知识,权当复习了。
在Android设备上,滑动几乎是应用的标配,不管是下拉刷新还是recyclerView和listView等控件的滑动,他们的基础都是滑动,不管哪种滑动,首先他们滑动的基本思想是一致的:当触摸事件传到View时,系统记录下触摸点的坐标,手指移动后系统也会记录下移动后的触摸点的坐标,然后算出偏移量,并通过偏移量来修改View的坐标。实现View的滑动目前来说主要有有以下三种方式:
1. 通过View本身提供的scrollTo/scrollBy方法来实现
2. 通过动画给View施加平移效果
3. 改变View的LayoutParams使的View重新布局
1.1 使用scrollTo和scrollBy实现View的滑动
先看源码解析:
/**
* 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方法,他实现了基于当前位置的相对滑动,也就是相对于父View左上角坐标位置移动到传进来的参数x,y加上他本身所在位置的坐标的位置。而scrollTo则是实现了基于所传参数的绝对滑动,也就是说相对于父View左上角坐标位置移动传进来的参数x,y的位置。
再换句话说:两种滑动方式的参照物不同,scrollBy是将本身作为参照物,scrollTo是将父View作为参照物,也可以这么记scrollBy就是滑动了,scrollTo就是滑动到,整个滑动的过程是:
在滑动的过程中,mScrollX的值总等于View左边缘和View内容左边缘在水平方向上的值。而mScrollY的值总等于View上边缘和View内容上边缘在垂直方向上的值。View边缘是指View的位置,即View的四个顶点组成,而View内容边缘是指View中的内容的边缘,scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。mScrollX和mScrollY的单位是像素,并且当View的左边缘在View的内容的左边缘的右边时,mScrollX是正值。反之为负值。也就是说,不管怎么滑动,View本身不能移动,只是将View的内容进行移动。
举个例子:
/**
* 触摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
//触摸点的坐标
int x= (int) event.getX();
int y= (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
//求得移动后的偏移量
int offsetX=x-lastX;
int offsetY=y-lastY;
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
case MotionEvent.ACTION_DOWN:
//记录移动后的触摸点的坐标
lastX=x;
lastY=y;
break;
default:
break;
}
return true;
}
1.2 使用动画实现View的滑动
通过动画我们能够让一个View进行平移,而平移本就是一种滑动。使用动画来移动View,主要操作还是View的translationX和transLationY属性。在这里我们可以使用传统的动画,也可以使用属性动画。
接下来分别采用两种方式将View在100ms内从原始位置移动到右下角100个像素的位置。
res——>anim——>translate.xml 的代码:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal"
>
<translate
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="100"
android:toYDelta="100"
android:duration="100"/>
</set>
activity中的使用:
//使用补间动画
btn.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate));
//使用属性动画
//ObjectAnimator.ofFloat(btn,"translationX",0,500).setDuration(10000).start();
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(AnimationActivity.this, "点击了Button", Toast.LENGTH_SHORT).show();
}
});
这里有两个很重要的点:
(1)补间动画属于View动画,即只对View的影像进行操作,并没有改变View的实际参数,包括宽高,并且,要想动画后的状态得以保留还必须将fillAfter属性值设置为true。否则动画完成后View就会恢复至原先的状态.通过我们的点击事件也可以验证出这个结果。
(2)属性动画可以解决此问题,但是无法兼容到Android3.0以下。
1.3 改变布局参数,即LayoutParams
LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局的参数从而达到了改变View的位置的效果。
/**
* 触摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
//触摸点的坐标
int x= (int) event.getX();
int y= (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
//求得移动后的偏移量
int offsetX=x-lastX;
int offsetY=y-lastY;
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
case MotionEvent.ACTION_DOWN:
//记录移动后的触摸点的坐标
lastX=x;
lastY=y;
break;
default:
break;
}
return true;
}
由于父控件是LinearLayout,所以我们用了LinearLayout里的LayoutParams,如果父控件是RelativeLayout则要使用RelativeLayout.LayoutParams。除了使用布局的LayoutParams外,我们还可以用ViewGroup.MarginLayoutParams来实现:
1.4 其他的几种方式
使用layout();offsetLeftAndRight()与offsetTopAndBottom()也可以实现滑动,具体使用方法和上面方法一致,在onTouch()事件中的MotionEvent.ACTION_MOVE下:
case MotionEvent.ACTION_MOVE:
//求得移动后的偏移量
int offsetX=x-lastX;
int offsetY=y-lastY;
layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY;
break;
或者
case MotionEvent.ACTION_MOVE:
//求得移动后的偏移量
int offsetX=x-lastX;
int offsetY=y-lastY;
//对left和right进行偏移
offsetLeftAndRight(offsetX);
//对top和bottom进行偏移
offsetTopAndBottom(offsetY);
1.5 弹性滑动
相对于普通的滑动方式来说,弹性滑动的方式就是实现渐进式的滑动,实现弹性滑动的方式有很多种,但他们具有一个共同的思想就是:将一次大的滑动分为若干次小的滑动,并在同一个时间段内完成,首先介绍Scroller。
1.5.1 使用Scroller
scroller的工作原理就是:当我们构造一个Scroller对象并且调用他的startScroll()方法时,Scroller内部其实什么也没做,他只是保存了传递的几个参数,我们从Scroller类的源码中就可以看到:
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
//duration表示的是整个滑动过程的完成所需要的时间,默认的滑动时间为250毫秒.
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
//startX和startY表示的是滑动的起点
mStartX = startX;
mStartY = startY;
//dx和dy分别表示要在横纵坐标上滑动的距离
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
这里需要注意:这里的滑动并非View本身位置的改变而是View内容的滑动,仅仅调用startScroll()方法是没法让View滑动的,真正的幕后是invalidate方法,他会导致View重绘,通过使View重绘,会间接的执行computeScroll()方法。实例解析:
private void smoothScrollTo(int dx,int dy){
//获取开始滑动时的坐标
int sX=this.getScrollX();
int sY=this.getScrollY();
//将参数保存到Scroller中
//dx-sX,dy-sY是横纵坐标滑动的距离,1000为整个滑动过程为1000毫秒,
scroller.startScroll(sX,sY,dx-sX,dy-sY,1000);
invalidate();
}
@Override
public void computeScroll() {
//判断滑动是否结束
if(scroller.computeScrollOffset()){
//getCurrX()返回当前的X轴偏移量,值等于当前View位置的左边界减去View内容的左边界。可以理解为View 中的mScrollX。
//getCurrY()值等于View位置的上边界减去view内容的上边界。类似于View中的mScrollY.
this.scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
而在源代码中computeScroll是一个空方法,需要我们自己实现,于是通过调用Scroller中的computeScrollOffset这个方法判断滑动是否结束,看一下computeScrollOffset方法:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
//执行动画已经花费的时间
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//当执行动画已经花费的时间小于整个滑动过程完成的时间的时候,返回为true,计算出mCurrX和mCurrY的值,也就是当前View内容左边缘的x、y坐标的值
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;
}
从源码注释中我们可以得知,当调用这个方法我们可以知道滑动的新的坐标位置,并且当其返回值为true时,滑动动画就没有结束,当执行动画已经花费的时间小于整个滑动过程完成的时间的时候,返回为true,计算出mCurrX和mCurrY的值,也就是当前View内容左边缘的x、y坐标的值,当返回为true时,调用scrollTo方法, this.scrollTo(scroller.getCurrX(),scroller.getCurrY());而scroller.getCurrX(),scroller.getCurrY()获得的就是mCurrX,mCurrY也就是View内容左边缘的x、y坐标的值,然后再调用invalidate(),直到computeScrollOffset返回false时,滑动结束,即整个滑动过程完成。
总结
通过回顾,发现有好多细节自己之前都没有注意到,所以还是老话说的好,温故而知新啊。如果有哪些点你觉得我的理解不对。欢迎留言指正,最近开通了自己的微信公众号,偶尔更新文章,生活感悟,好笑的段子,欢迎订阅
文章中所用到的demo的下载地址
参考资料:
《Android开发艺术探索》
上一篇: 【View基础知识】View滑动之LayoutParams
下一篇: View几种滑动的方式