最近想总结一下,关于View滑动的知识,也为下一篇View的滑动总结记录一下这个知识点点吧。提到这个,先说说Android的坐标系吧。
Android中的坐标系
Android中有2种坐标系,分别称之为Android坐标系和视图坐标系。而对应的也有一些相关的方法可以获取坐标系中的坐标值,只有搞清楚这些区别,才能在使用的时候,不至于出错或者得不到你想要的效果。
1.Android坐标系
如图所示,Android以屏幕左上角位坐标原点,从该点向右为X轴的正方向,向下为Y轴的正方向,在我们处理触屏事件的时候,使用getRawX()/getRawY(),都是相对于这个坐标原点的坐标,也就是绝对坐标啦。
2.视图坐标系
视图坐标系描述的是子View相对于父View的相对位置坐标
如上图所示,在这里我们可以用getX(),getY(),获取的是子视图相对于父视图的坐标位置。PS:我在开发中还是喜欢使用getRawX或者getRawY获取坐标值,这样就避免去计算相对坐标那么多麻烦的计算了额。
scrollTo和scrollBy
scrollTo(int x,int y),从字面意思,我们可以理解为滚动到某个位置,这里就是滚动到x,y位置
scrollBy(int x,int y),从字面意思,我们可以理解为滚动某一段距离,这里的意思就是相对于当前位置X方向上再滚动x距离,Y方向上在滚动y距离。
看看相关源码:
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);
}
在scrollTo()方法中,入口就是判断了mScrollX!=x || mScrollY !=y; 其中x,y是我们自己传入的值,那么mScrollX和mScrollY是什么呢
public final int getScrollX() {
return mScrollX;
}
/**
* Return the scrolled top position of this view. This is the top edge of
* the displayed part of your view. You do not need to draw any pixels above
* it, since those are outside of the frame of your view on screen.
*
* @return The top edge of the displayed part of your view, in pixels.
*/
public final int getScrollY() {
return mScrollY;
}
其实mScrollX和mScrollY就是View滚动的X方向和Y方向的距离。二这种距离与我们平时的理解有点区别,简单的理解和记忆就是View向右滑动mScrollX为负值,向下滑动mScrollY为负值,反之为正值,与平时的认知哟独爱你相反的感觉。
scrollTo和scrollBy移动的方式实际是内容的移动,也就是当你当前调用这两个方法的View位一个ViewGroup的时候,实际就是这个ViewGroup里面的子View的移动,如果你调用这两个方法的View是一个子View,例如TextView调用这个两个方法的时候,实际就是TextView里面的文本内容的移动了。
mScrollX 的计算方式是,View的左边缘的位置-View内容的左边缘 ,所以向右移动时,View的内容向右移动,它的X坐标肯定大于View的X坐标,所以其差值位负值。
Scroller实现弹性滑动
在上面我们看到其实调用View的scrollTo/scrollBy方法,其实都是使我们空间在瞬间移动到某个位置,这给人的感觉很不好,不能实现平滑的滑动,因此此时的Scroller就应运而生了额。说道Scroller,其实他与前面的scrollTo和scrollBy实现原理基本类似。
关于Scroller的开发步骤
1.初始化Scroller,一般在自定义View的构造函数里面初始化,避免重复创建。
mScroller = new Scroller(context);
2。重写computeScroll()方法。
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
// ((View)getParent()).scrollTo();
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
在Scroller中提供了computeScrollOffset()方法,这个方法是用来判断整个滑动是否完成。在computeScroll法法中调用invalidate()方法,这其实是一个递归循环的调用invalidate()->onDraw()->computeScroll()这样才能实现平滑的滑动。
3.startScroll开启模拟过程
Scroller 是由startScroll()方法开启滑动的,他有两个重载方法
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_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;
}
他们两个函数的差别就在与一个有duration参数,一个没有。我们看到其实在这个方法里面也只是对一些坐标进行赋值。没有看到我们控件的实际滑动。其实控件的滑动是需要在startScroll()之后调用View的invalidate();view的重绘其实会间接的调用computeScroll()方法。
/**
* 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);
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表示未结束,false表示已经结束。可以看到这里面也是计算坐标的。当timePassed<duration的时候,根据时间和插值器计算出mCurrX,mCurrY的值,并且函数返回true。也就代表滑动没有结束。而computeScroll方法中调用scrollTo(mScroller.getCurrX(),mScroller.getCurrY()),其实就是调用了scrollTo(mCurrX,mCurrY);这里的mCurrX和mCurrY就是上面计算的值。
通过上面的分析可以得出,通过Scroller,并且按照上面的三个步骤去完成我们的滑动就没有问题了。
下面以一个简单的例子,来实现类似系统ViewPager的功能,这里也借鉴了一下郭霖关于Scroller的讲解,谢谢作者,当然也不完全相同,这里只是为了记录一下相关写法和步骤,以便自己以后方便阅读和查阅。
public class ScrollerLayout extends ViewGroup {
private Scroller mScroller;
private int mTouchSlop;
private float lastDownX,lastDownY;
private int leftBorder,rightBorder;
public ScrollerLayout(Context context) {
super(context);
initView(context);
}
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public ScrollerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context){
mScroller = new Scroller(context);
//表示滑动的手指需要移动的最小距离
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(changed){
int count = getChildCount();
for(int i=0;i<count;i++){
View childView = getChildAt(i);
if(childView !=null){
// view.layout(view.getMeasuredWidth()*i,0,(i+1)*view.getMeasuredWidth(),getMeasuredHeight());
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
// 初始化左右边界值 防止最左边的View和最右边的View滑出边界了
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(getChildCount() - 1).getRight();
}
@Override
protected 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);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
float x = ev.getRawX();
float y = ev.getRawY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:{
lastDownX = x;
lastDownY = y;
break;
}
case MotionEvent.ACTION_MOVE:{
float deltX = Math.abs(x-lastDownX);
if(deltX>mTouchSlop){
return true;
}
break;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{ //如果内部控件没有设置cliclable为ture,就需要拦截down的操作
return true;
}
case MotionEvent.ACTION_MOVE:{
int dx = (int)(lastDownX-event.getRawX());
if(getScrollX()+dx<leftBorder){ //防止滑动超出左边界
scrollTo(leftBorder,0);
return true;
}else if(getScrollX()+getWidth()+dx>rightBorder){ //防止滑动超出右边界
scrollTo(rightBorder-getWidth(),0);
return true;
}
scrollBy(dx,0);
//这句话必须要,否则滑动就不能产生了
lastDownX = event.getRawX();
break;
}
case MotionEvent.ACTION_UP:{
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
if(dx<getWidth()/2){ //这里是计算 我们滑动的距离如果小于一半,松开手之后就回去了
mScroller.startScroll(getScrollX(),0,dx,0);
}else{ //滑动距离超过一半,松开手指之后,就惯性滑动一整个屏幕。
mScroller.startScroll(getScrollX(), 0, dx, 0);
}
invalidate();
break;
}
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
// ((View)getParent()).scrollTo();
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
代码不难,注释也写的很清楚了额,不再详细解释了。Demo传送门