前面一篇博文《关于View的ScrollTo, getScrollX 和 getScrollY》,我们讲解View 的scrollTo() 和 getScrollX() 的功能,也提到了它们其实是一般是配合scroller 类来进行屏幕的滑动的。有的朋友可能会问,为什么有了scrollTo() 之后,还要有scroller 类呢,区别在于,scrollTo() 和 scrollBy() 他们实现的是一个结果,也就是说,当你调用scrollTo(100, 0) 的时候,再重新绘制的时候,内容就已经出现在(100,0)的位置上了,缺少一个过程,而scroller 类就是来帮助我们展现这个滚动的过程的。
动画的原理其实是不停地重绘位置变化的内容,在视觉效果上,就会产生动画的效果。scroller 类的原理其实也正是如此,通过循环地绘制不同位置上的内容,来展现屏幕滚动。
它的原理图大概如下:
这样讲就太抽象了,还是结合例子来看看吧。
拿上次写的自定义ViewGroup, 我们来进行扩展一下,代码如下:
public class CustomRotateViewGroup extends ViewGroup implements OnTouchListener{ private static final int snapVelocity = 200; private static final int rows = 3; private static final int padding = 10; private Scroller mScroller; private float downX; private float curX; private int distanceX; private int screenWidth; private VelocityTracker velocityTracker; private enum Direction{ Current, Next } public CustomRotateViewGroup(Context context) { super(context); initialize(context); } public CustomRotateViewGroup(Context context, AttributeSet attrs) { super(context, attrs); initialize(context); } //初始化操作 public void initialize(Context context){ DisplayMetrics displayMetrics = new DisplayMetrics(); WindowManager windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); windowManager.getDefaultDisplay().getMetrics(displayMetrics); screenWidth = displayMetrics.widthPixels;//获得屏幕宽度 mScroller = new Scroller(context);//获取scroller实例 setOnTouchListener(this);//设置触摸监听 //添加图片 addCustomRotateView(context, R.drawable.photo1, 60, Color.YELLOW); addCustomRotateView(context, R.drawable.photo2, 30, Color.CYAN); addCustomRotateView(context, R.drawable.photo3, -30, Color.MAGENTA); addCustomRotateView(context, R.drawable.photo4, 60, Color.CYAN); addCustomRotateView(context, R.drawable.photo5, 30, Color.MAGENTA); addCustomRotateView(context, R.drawable.photo6, -30, Color.LTGRAY); addCustomRotateView(context, R.drawable.photo3, 60, Color.YELLOW); addCustomRotateView(context, R.drawable.photo4, 30, Color.LTGRAY); addCustomRotateView(context, R.drawable.photo5, -30, Color.CYAN); } private void addCustomRotateView(Context context, int resId, float degree, int color){ addView(new CustomRotateView(context, resId, degree, color)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ super.onMeasure(widthMeasureSpec, heightMeasureSpec); int len = getChildCount(); int parentHeight = MeasureSpec.getSize(heightMeasureSpec); parentHeight -= padding * rows; for(int i = 0; i < len; i++){ View child = getChildAt(i); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentHeight / rows, MeasureSpec.AT_MOST); measureChild(child, widthMeasureSpec, childHeightMeasureSpec); } } //布局函数,将图片从左向右排列,一列3个。 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int startX = 0; int startY = 0; int len = getChildCount(); boolean flagVertical = true; for(int i = 0; i <len; i++){ View child = getChildAt(i); int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); child.layout(startX, startY, startX + childWidth, startY + childHeight); if(flagVertical){ if((i + 1) % rows == 0){ startX += childWidth; flagVertical = false; startY = 0; }else{ startY += childHeight + padding; } }else{ flagVertical = true; startY += childHeight + padding; } } } //重载View的computeScroll函数 @Override public void computeScroll(){ if(mScroller.computeScrollOffset()){//滚动是否结束,返回true表明滚动还未结束,同时在函数里获取currX和currY的值 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());//滚动到当前位置 postInvalidate();//强制重新绘制View,在框架里,View 的绘制会重新调用到computeScroll方法,达到循环绘制的目的 } } @Override public boolean onTouch(View v, MotionEvent event) { //to add the event to the velocityTracker, in order to calculate the velocity of the slop if(velocityTracker == null){ velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: //当停止动画的时候,它会马上滚动到终点,然后向动画设置为结束。 if(mScroller != null && !mScroller.isFinished()){ mScroller.abortAnimation(); } downX = event.getX(); curX = event.getX(); break; case MotionEvent.ACTION_MOVE: float moveX = event.getX(); distanceX = (int)(moveX - curX); scrollBy(-distanceX, 0); curX = moveX; break; case MotionEvent.ACTION_UP: float upX = event.getX(); distanceX = (int)(upX - downX); velocityTracker.computeCurrentVelocity(1000); float vx = velocityTracker.getXVelocity(); if (getScrollX() < 0) { // scroll to right (the first screen) mScroller.startScroll(getScrollX(), 0, 0 - getScrollX(), 0,1000); } else if (getScrollX() > screenWidth * (getChildCount() / 3 - 1)) { // scroll to left scrollToScreen(Direction.Current); } else { if (distanceX > 0) { // scroll to left if (distanceX > screenWidth / 2 || vx > snapVelocity) { scrollToScreen(Direction.Current); } else { scrollToScreen(Direction.Next); } } else if (distanceX < 0) { if (Math.abs(distanceX) > screenWidth / 2 || vx < -snapVelocity) { // scroll to right (the next screen) scrollToScreen(Direction.Next); } else { // scroll to left (the current screen) scrollToScreen(Direction.Current); } } } velocityTracker.recycle(); invalidate();// 在这里一定要记得调用invalidate 来促使屏幕重绘,开始循环绘制的过程,不然。。。没有入口去滚。 break; } return true; } private void scrollToScreen(Direction to){ if(to == Direction.Next){ mScroller.startScroll(getScrollX(), 0, screenWidth - getScrollX() % screenWidth, 0, 1000); }else{ mScroller.startScroll(getScrollX(), 0, - getScrollX() % screenWidth, 0, 1000); } } }
这个自定义的滚动ViewGroup 实现的主要功能如下:
1)首先我们添加了N(在这个例子中是9) 张自定义的图片放在这个ViewGroup里面
2)将这 N 张图片按照 3 个一列,从左向右排,为了易于展示和写代码,每一列都跟屏幕一样宽,这样在界面上的时候,就只看到第一列图片
3)当我们手指向左向右滑动的时候,屏幕会跟随着手指进行滑动
4)当我们手指滑过窗口的二分之一宽或者滑动的速度大于200的时候,手指松开,屏幕会自动滑向下一屏或者上一屏。
5)边界检测,当屏幕滑到最左屏最右屏的时候,再继续滑动,到达黑暗地域的时候,手指松开,会自动滚动回来。
下面是效果Gif:(之前滑动效果做反了,经同事提醒,才发现的确是有点不对劲,=_=!! ---修正于20140112)
最后附上源代码,请点击 源代码下载,希望各位指出需要完善的地方,一起进步。