滚动视图的方法有两种:
scrollTo
和scrollBy
,而Scroller
就是它们的辅助工具类,所以Scroller
是学好高级UI必不可少的一课。
(1)scrollTo、scrollBy、getScrollX、getScrollY
view的内容本身具备滚动的方法,其中滚动方法如下:
- scrollTo:相对于初始位置移动
- scrollBy:相对于上次移动的最后位置移动
这两个方法特别需要注意以下几点:
- 两者移动的都是view的内容,view本身是不移动的,所以getX和getY的值不会受到这两个方法的影响;
- 不要再在
onDraw
中调用这两个方法,避免onDraw
方法被重复执行,因为一旦调用这两个方法view会被重绘,onDraw
方法会再次执行。
view内容滚动的方法有了,那么该如何获取view内容被滚动的距离呢?看以下两个方法:
- getScrollX:获取view的内容在X轴滚动的距离
- getScrollY:获取view的内容在Y轴滚动的距离
以上只说到view内容的滚动,那么view本身的移动用什么方法呢?
答:setX
和setY
方法。
本文的重点内容是Scroller
,这个辅助类的作用不是view本身的移动,而是view内容的滚动,下面开始简单说明一下Scroller
辅助类。
(2)熟悉Scroller的构造方法
//默认插值器是ViscousFluidInterpolator
Scroller mScroller = new Scroller(mContext);
//指定一个插值器
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator());
//指定一个插值器,第三个参数表示是否开启“飞轮”效果,也就是多次滚动时速度叠加
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator(), false);
(3)熟悉插值器
Scroller其实就是在scrollTo(x, y)
和scrollBy(x, y)
的基础上添加滚动效果,滚动效果是一个动画,当我们new
一个Scroller对象时,就已经指定了一个插值器,下面来说明一下各种插值器:
-
ViscousFluidInterpolator
:这是一个默认插值器,当构造Scroller
时,如果不传递插值器或者插值器为null时,系统默认使用ViscousFluidInterpolator
插值器。
-
AccelerateDecelerateInterpolator
:在动画开始与结束的时候速率改变比较慢,在中间的时候速率较快。
-
AccelerateInterpolator
:在动画开始的地方速率改变比较慢,然后开始加速。
AnticipateInterpolator
:开始的时候向后然后向前甩。AnticipateOvershootInterpolator
:开始的时候向后然后向前甩一定值后返回最后的值。BounceInterpolator
:反弹插值器。CycleInterpolator
:动画循环播放特定的次数,速率改变沿着正弦曲线。DecelerateInterpolator
:在动画开始的地方快然后慢。LinearInterpolator
:以常量速率改变。
OvershootInterpolator
:向前甩一定值后再回到原来位置。-
PathInterpolator
:路径插值器,我们可以按照自己想要的轨迹滚动。PathInterpolator(Path path)
PathInterpolator(float controlX, float controlY)
PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2) FastOutLinearInInterpolator
:MaterialDesign基于贝塞尔曲线的插补器效果:依次慢慢快。
-
FastOutSlowInInterpolator
:基于贝塞尔曲线的插补器效果:依次慢快慢
-
LinearOutSlowInInterpolator
:基于贝塞尔曲线的插补器效果:依次快慢慢
以上的插值器运用比较广泛,在Scroller
中设置一个插值器可以优化滚动的效果。
(4)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)
fling()
惯性滑动,参数如下:
startX:滚动起始点x
startY:滚动起始点y
velocityX:x轴方向的速度
velocityY:y轴方向的速度
minX:x轴最小滚动距离(注意直角坐标系)
maxX:x轴最大滚动距离(注意直角坐标系)
minY:y轴最小滚动距离(注意直角坐标系)
maxY:y轴最大滚动距离(注意直角坐标系)
computeScrollOffset()
判断滚动动画是否结束:
true:滚动尚未完成
false:滚动已经完成
(5)Scroller实现滚动效果
public class TestView extends View {
private float mDownX = 0;
private float mDonwY = 0;
private float move_x = 0;
private float move_y = 0;
private int finalX = 0;
private int finalY = 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:
//按住一点手指开始移动
move_x = mDownX - x;//计算当前已经移动的x轴方向的距离
move_y = mDonwY - y;//计算当前已经移动的y轴方向的距离
//开始滚动动画
//第一个参数:x轴开始位置
//第二个参数:y轴开始位置
//第三个参数:x轴偏移量
//第四个参数:y轴偏移量
mScroller.startScroll(finalX, finalY, (int) move_x, (int) move_y, 0);
invalidate();//目的是重绘view,是的执行computeScroll方法
break;
case MotionEvent.ACTION_UP:
finalX = mScroller.getFinalX();
finalY = mScroller.getFinalY();
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){//判断滚动是否完成,true说明滚动尚未完成,false说明滚动已经完成
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//将view直接移动到当前滚动的位置
invalidate();//触发view重绘
}
}
}
效果如下:
当发生ACTION_MOVE事件时,执行startScroll方法开始滚动view,由于ACTION_MOVE事件发生的特别频繁,所以startScroll方法的最后一个参数设置为0ms。
当然,可以将startScroll方法放在ACTION_UP事件中执行,调整代码:
@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:
//按住一点手指开始移动
move_x = mDownX - x;//计算当前已经移动的x轴方向的距离
move_y = mDonwY - y;//计算当前已经移动的y轴方向的距离
break;
case MotionEvent.ACTION_UP:
finalX = mScroller.getFinalX();
finalY = mScroller.getFinalY();
//开始滚动动画
//第一个参数:x轴开始位置
//第二个参数:y轴开始位置
//第三个参数:x轴偏移量
//第四个参数:y轴偏移量
mScroller.startScroll(finalX, finalY, (int) move_x, (int) move_y, 3000);
invalidate();//目的是重绘view,是的执行computeScroll方法
break;
}
return true;
}
代码的意思是:从一个坐标到另一个坐标的滑动需要3秒时间,当手指松开时开始执行滚动动画,动画时长为3秒。
效果如下:
(6)Scroller实现惯性滚动效果
惯性滚动是指,手指松开view后,根据当前速度再滑动一段距离,就跟惯性类似。
实现惯性滚动效果
的方法是fling()
,执行fling()
方法的时机是MotionEvent.ACTION_UP
事件,代码实现如下:
public class TestView extends View {
//惯性滑动速度追踪类
private VelocityTracker velocityTracker;
private float mDownX = 0;
private float mDonwY = 0;
private float move_x = 0;
private float move_y = 0;
private int finalX = 0;
private int finalY = 0;
private int xVelocity = 0;
private int yVelocity = 0;
private Paint mPaint;
private Scroller mScroller;
private OverScroller mOverScroller;
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);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(50);
mScroller = new Scroller(mContext);
mOverScroller = new OverScroller(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坐标值
//创建惯性滑动速度追踪类对象
velocityTracker = VelocityTracker.obtain();
break;
case MotionEvent.ACTION_MOVE:
//按住一点手指开始移动
move_x = mDownX - x;//计算当前已经移动的x轴方向的距离
move_y = mDonwY - y;//计算当前已经移动的y轴方向的距离
//开始滚动动画
//第一个参数:x轴开始位置
//第二个参数:y轴开始位置
//第三个参数:x轴偏移量
//第四个参数:y轴偏移量
if(mScroller.isFinished()){
mScroller.startScroll(finalX, finalY, (int) move_x, (int) move_y, 0);
}
invalidate();//目的是重绘view,是的执行computeScroll方法
//将事件加入到VelocityTracker类实例中
velocityTracker.addMovement(event);
//计算1秒内滑动的像素个数
velocityTracker.computeCurrentVelocity(1000);
//X轴方向的速度
xVelocity = (int) velocityTracker.getXVelocity();
//Y轴方向的速度
yVelocity = (int) velocityTracker.getYVelocity();
break;
case MotionEvent.ACTION_UP:
//获取认为是fling的最小速率
int mMinimumFlingVelocity= ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity() / 10;
if (Math.abs(xVelocity) >= mMinimumFlingVelocity || Math.abs(yVelocity) > mMinimumFlingVelocity) {
Log.d("yunchong", "触发惯性滑动");
mScroller.fling(getScrollX(), getScrollY(), -xVelocity, -yVelocity, -getWidth()+100, 0, -getHeight()+100, 0);
} else {//缓慢滑动不处理
}
finalX = mScroller.getFinalX();
finalY = mScroller.getFinalY();
velocityTracker.recycle();
velocityTracker.clear();
velocityTracker = null;
break;
case MotionEvent.ACTION_CANCEL:
velocityTracker.recycle();
velocityTracker.clear();
velocityTracker = null;
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){//判断滚动是否完成,true说明滚动尚未完成,false说明滚动已经完成
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//将view直接移动到当前滚动的位置
invalidate();//触发view重绘
}
}
}
效果展示:
[本章完...]