Android Scroller分析
(1)Android view的直角坐标系
图片.png需要注意的是,view的直角坐标系和数学的直角坐标系不同,view的x轴方向和数学的直接坐标系一致,但是y轴方向却相反。
(2)基本方法
在介绍Scroller
之前,我们先来看一下view移动的基础方法。
-
getScrollX()
和getScrollY()
获取x轴方向和y轴方向偏移量
,如view直角坐标系所示,view的左上角在原点
处,也就是说,初始状态下 getScrollX()
和 getScrollY()
的值为0,往左移动时偏移量x为负数,往右移动时偏移量x为正数,往上移动时偏移量y为负数,往下移动时偏移量y为正数。
-
scrollTo(x, y)
(无滚动特效,所以叫移动)
将view的内容
移动到指定位置。
这里需要注意的是,不是移动view,而是移动view的内容。
这个方法往往结合onTouchEvent
一起使用,我们看以下代码
public class TestView extends View {
private float mLastX = 0;
private float mLastY = 0;
private Paint mPaint;
public TestView(Context context) {
super(context);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setTextSize(80);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText("我是中国人!", 0, 100, mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//标志着第一个手指按下
mLastX = x;//获取按下时x坐标值
mLastY = y;//获取按下时y坐标值
break;
case MotionEvent.ACTION_MOVE:
//按住一点手指开始移动
float move_x = mLastX - x;//计算当前已经移动的x轴方向的距离
float move_y = mLastY - y;//计算当前已经移动的y轴方向的距离
float oldScollX = getScrollX();//计算之前已经偏移的x轴方向的距离
float oldScollY = getScrollY();//计算之前已经偏移的y轴方向的距离
scrollTo((int) move_x, (int) move_y);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//表示手势被取消了,不再接受后续事件
scrollTo(0, 0);
break;
}
return true;
}
}
我们自定义一个view,view仅仅绘制一个文本,这个文本就是view的内容,代码的逻辑是:移动view,在移动的过程中view的内容也随之移动,当结束移动时,view的内容位置恢复。
图片效果:
46.gif-
scrollBy(x, y)
(无滚动特效,所以叫移动)
我们先来看一下scrollTo,假如我们去掉触摸时间的处理
public class TestView extends View {
private Paint mPaint;
public TestView(Context context) {
super(context);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setTextSize(80);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText("我是中国人!", 0, 100, mPaint);
}
}
这样的话文本的位置是:
图片.png
现在我们使用代码控制view内容的位置
tv_text = findViewById(R.id.tv_text);
tv_text.scrollTo(-100, -100);
移动之后的效果如下:
图片.png当我们多次使用scrollTo
后
tv_text = findViewById(R.id.tv_text);
tv_text.scrollTo(-100, -100);
tv_text.scrollTo(-100, -100);
tv_text.scrollTo(-100, -100);
tv_text.scrollTo(-100, -100);
tv_text.scrollTo(-100, -100);
效果如下:
图片.png我们发现不管我们使用多少次scrollTo
,view的移动都是以最开始的位置开始的。
scrollBy(x, y)
可以完美解决这个问题,我们看一下源码就知道它的具体作用了:
/**
* 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(x, y)
使每次移动都是以当前位置开始。
tv_text = findViewById(R.id.tv_text);
tv_text.scrollBy(-100, -100);
tv_text.scrollBy(-100, -100);
tv_text.scrollBy(-100, -100);
tv_text.scrollBy(-100, -100);
tv_text.scrollBy(-100, -100);
tv_text.scrollBy(-100, -100);
tv_text.scrollBy(-100, -100);
效果如下:
图片.png(3)Scroller
滑动辅助类的基本方法
Scroller本身不会去移动view,它只是一个移动计算辅助类,用于跟踪控件滑动的轨迹,只相当于一个滚动轨迹记录工具,最终还是通过View的scrollTo、scrollBy方法完成View的移动的。
getCurrX()
获取mScroller当前水平滚动的位置
getCurrY
获取mScroller当前竖直滚动的位置
getFinalX
获取mScroller最终停止的水平位置
getFinalY
获取mScroller最终停止的竖直位置
startScroll()
开始滚动动画:
startX:滚动的x方向起始点
startY:滚动的y方向起始点
dx:x方向的偏移量
dy:y方向的偏移量
duration:滚动所消耗的时间,默认为250毫秒
startScroll(int startX, int startY, int dx, int dy)
startScroll(int startX, int startY, int dx, int dy, int duration)
computeScrollOffset()
判断滚动动画是否结束:
true:滚动尚未完成
false:滚动已经完成
(4)基本代码实现
public class TestView extends View {
private float mDownX = 0;
private float mDonwY = 0;
private Paint mPaint;
private Scroller mScroller;
public TestView(Context context) {
super(context);
init(context);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context mContext){
mPaint = new Paint();
mPaint.setTextSize(80);
mScroller = new Scroller(mContext);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText("我是中国人!", 0, 100, mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//标志着第一个手指按下
mDownX = x;//获取按下时x坐标值
mDonwY = y;//获取按下时y坐标值
break;
case MotionEvent.ACTION_MOVE:
//按住一点手指开始移动
float move_x = mDownX - x;//计算当前已经移动的x轴方向的距离
float move_y = mDonwY - y;//计算当前已经移动的y轴方向的距离
float oldScollX = getScrollX();//计算之前已经偏移的x轴方向的距离
float oldScollY = getScrollY();//计算之前已经偏移的y轴方向的距离
//开始滚动动画
//第一个参数:x轴开始位置
//第二个参数:y轴开始位置
//第三个参数:x轴偏移量
//第四个参数:y轴偏移量
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), (int) move_x, (int) move_y, 3000);
invalidate();//目的是重绘view,是的执行computeScroll方法
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){//判断滚动是否完成,true说明滚动尚未完成,false说明滚动已经完成
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//将view直接移动到当前滚动的位置
invalidate();//触发view重绘
}
}
}
效果如下:
47.gif看到这三秒钟的滚动动画了吧,默认情况下就是这个效果,默认情况下Scroller
使用的插值器是ViscousFluidInterpolator
,从字面意义上看是一个粘性流体插值器。
(5)构造方法
//默认插值器是ViscousFluidInterpolator
Scroller mScroller = new Scroller(mContext);
//指定一个插值器
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator());
//指定一个插值器,第三个参数表示是否开启“飞轮”效果,也就是多次滚动时速度叠加
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator(), false);
(6)插值器
图片.pngScroller其实就是在scrollTo(x, y)
和scrollBy(x, y)
的基础上添加滚动效果,滚动效果是一个动画,当我们new
一个Scroller对象时,就已经指定了一个插值器,下面来说明一下各种插值器:
ViscousFluidInterpolator
这是一个默认插值器,当构造Scroller
时,如果不传递插值器或者插值器为null时,系统默认使用ViscousFluidInterpolator
插值器。
AccelerateDecelerateInterpolator
在动画开始与结束的时候速率改变比较慢,在中间的时候速率较快。
AccelerateInterpolator
在动画开始的地方速率改变比较慢,然后开始加速。
AnticipateInterpolator
开始的时候向后然后向前甩。
如图所示
48.gifAnticipateOvershootInterpolator
开始的时候向后然后向前甩一定值后返回最后的值。
如图所示:
49.gifBounceInterpolator
反弹插值器。
如图所示:
50.gif
CycleInterpolator
动画循环播放特定的次数,速率改变沿着正弦曲线。
51.gifDecelerateInterpolator
在动画开始的地方快然后慢。
52.gifLinearInterpolator
以常量速率改变。
52.gifOvershootInterpolator
向前甩一定值后再回到原来位置.
PathInterpolator
路径插值器,我们可以按照自己想要的轨迹滚动。
PathInterpolator(Path path)
PathInterpolator(float controlX, float controlY)
PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2)
如果学习Path使用的话,这篇博客是个不错的选择Android开发之Path详解
FastOutLinearInInterpolator
MaterialDesign基于贝塞尔曲线的插补器效果:依次慢慢快。
FastOutSlowInInterpolator
基于贝塞尔曲线的插补器效果:依次慢快慢
LinearOutSlowInInterpolator
基于贝塞尔曲线的插补器效果:依次快慢慢
以上的插值器运用比较广泛,在Scroller
中设置一个插值器可以优化滚动的效果。