1 代码示例
Scroller是用于View弹性滑动的工具类,可以平滑的移动view的位置,而不会产生突变的效果。ViewPager和ListView都使用到了该类。实际上Scroller里面使用到了View类的scrollTo()和scrollBy()这两个方法,前者滑动的是绝对坐标,之后为相对坐标。下面这幅图是调用ViewGroup的scrollTo(100,100)的效果,黄色区域是ViewGroup,绿色框是其内容。
可以看到,如果调用scroller(x,y),x>0则其内容往左移动,y>0则往上移动,getScrollX()=x,getScrollY()=y。
我们来看一个例子,将一个图片(ButtonView)随着手指滑动而移动,松手之后弹性滑动会原来的位置。
xml文件布局如下:
<RelativeLayout 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:id="@+id/cl_top"
tools:context="com.study.test.CircleActivity"
android:layout_margin="50dp">
<com.study.test.view.MyViewGroup
android:background="@color/colorPrimary"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
>
<com.study.test.view.ButtonView
android:layout_centerInParent="true"
android:layout_height="100dp"
android:layout_width="100dp"
android:src="@drawable/meitu"
>
</com.study.test.view.ButtonView>
<TextView
android:text="我是个托"
android:textSize="50dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_marginTop="50dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:background="#ffffff"
>
</TextView>
</com.study.test.view.MyViewGroup>
</RelativeLayout>
外层用了一个自定义的ViewGroup背景为蓝色(继承自RelativeLayout),里面是自定义的ButtonView以及系统自带的TextView,界面效果如下图。
我们接着看自定义ButtonView:
public class ButtonView extends android.support.v7.widget.AppCompatImageView {
……
ViewGroup viewGroup;
int startX;
int startY;
Scroller mScroller = new Scroller(this.getContext());
@Override
protected void onAttachedToWindow() {
viewGroup = (ViewGroup) getParent();
super.onAttachedToWindow();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) event.getX();
startY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int curX = (int) event.getX();
int curY = (int) event.getY();
int delX = curX-startX;
int delY = curY-startY;
startX = curX;
startY = curY;
viewGroup.scrollBy(-delX,-delY);
break;
case MotionEvent.ACTION_UP:
mScroller.startScroll(viewGroup.getScrollX(),viewGroup.getScrollY(),
-viewGroup.getScrollX(),-viewGroup.getScrollY(),3000);
invalidate();
break;
default:
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
viewGroup.scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
}
首先是在MotionEvent.ACTION_MOVE中使用scrollBy进行移动,如果手指向右滑动,则delX>0,要ButtonView向右移动,则scrollerX<0,所以scrollBy得参数为-delX和-delY。
从图中看到,在ButtonView随着手指移动时,有很明显的跳跃感,不连贯。手指抬起之后的移动就很平滑。另外,我只是在ButtonView调用了viewGroup.scrollBy(-delX,-delY)和mScroller方法,但是同一级的TextView页跟着滑动了,这是因为滑动的是viewGroup的内容,而不是它本身,它本身的蓝色背景并没有任何变化。
2 源码分析
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
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;
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
viewGroup.scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
startScroll中startX,startY表示起点位置,需要设置为当前位置,不然会直接从当前位置跳到起点位置之后执行移动。如果是不断调用startScroll,可以通过getFinalX()这样获取前一个scroll完成之后的终点位置,作为起点。
AnimationUtils.currentAnimationTimeMillis()获取当前移动动画开始的时间mStartTime ,结合动画持续时间duration,也就是说在 mStartTime -> mStartTime+duration 时间内,从startX移动到startX+dx。
我们通过不断的重绘,每次重绘都根据当前时间与mStartTime的差值,计算出当前需要滑动到的位置,就可以实现弹性滑动了。怎样才能不断重绘呢,系统在每次调用invalidate重绘都会走到View的computeScroll方法中,我们只需要在重写该方法,在其中作判断,还没有移动到目的地,就进行移动,并且调用invalidate,则在动画完成之前,会不断重绘。