android程序中,大部分比较炫酷UI,都是基于SCroller或者是动画来实现的。本文主要讲解下Scroller类是如何配合View组件来使用的。
我们首先来看下View.scrollTo(int x,int y)和View.ScrollBy(int dx,int dy)两个方法:这两个方法其实很简单:
scrollBy方法是从在当前的坐标数值上,滑动传入的参数dx,dy的那么多的距离。
说白了一个是滑动到具体的坐标点,一个是从当前内容的坐标开始滑动一个传入的参数的距离。注意,这里说的滑动和动画中的滑动不是一个概念:动画中的滑动是将View组件整体的坐标值改变来进行滑动,改变了View组件在屏幕中的坐标点,而Scrollto或者ScrollBy中的滑动,是滑动的组件的内容。所以嘛,一般能用到scrollto或者scrollby这两个方法的组件,肯定都是内容比较大,超出屏幕显示的。比如android常用的scrollView或者HorizontalScrollView 组件,就是基于这两个方法来的。
那这两个方法一般在什么情形下去使用呢?其实用膝盖想也知道是onTouchEvent();在onTounchEvent()中使用监听事件计算手势划过的距离,然后调用scrollBy()方法,然View的内容进行滑动。关于滑动参数的正负型:x轴,正数代表内容向左滑动,负数代表内容向右滑动。y轴:正数代表内容向上滑动,负数代表向下滑动。
了解这两个方法之后,我们来说说View.getScrollX()和View.getScrollY()的含义。其实很简单,scrollX就是代表View左边缘距离手机屏幕左边的宽大小,scrollY就是代表View上边缘距离手机屏幕右边的大小(感谢任玉刚主席书中的解释!)所以,可以这样理解:从左向右滑动的时候,getScrollX为负值,反之亦然;从上向下滑动的时候,getScrollY为正数值,反之亦然。
好吧,现在扯了那么多,说好的Scroller类呢?这就开始说Scroller类,其实上面的篇章将的都是View关于滑动的动作,而Scroller类就是辅助计算View滑动的,也就是说,Scroller类是一个辅助计算类。也许你会问:这不是搞笑吗?我们既然能在onTouchEvent中计算滑动的距离,还需要Scroller类吗?其实,ScrollBy或者ScrollTo确实都是配合手势来用的,但是这些滑动都是瞬间的滑动,并没有给用户滚动的过程,其实造成的用户体验不是很好,所以需要使用到scroller类。
我们可以思考如下场景:
这些问题都是涉及到比较复杂的滑动计算的,而这些滑动计算的功能,都需要Scroller类来帮助我们计算。
其实刚刚接触到Scroller类的人都会觉得很难理解:其实我们要思考清楚两个问题:
package slidingmenu.dreamfly.org.slidingmenu.custom.app;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.BounceInterpolator;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.OverScroller;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.Scroller;
import slidingmenu.dreamfly.org.slidingmenu.R;
/** * Created by asus on 2015/10/20. */
public class DetailRootLayout extends LinearLayout {
private View mTop;
private View mBottom;
private int mTopViewHeight;
private OverScroller mScroller;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMaximumVelocity, mMinimumVelocity;
private float mLastY;
private boolean isDragging;
public DetailRootLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(LinearLayout.VERTICAL);
this.mVelocityTracker=VelocityTracker.obtain();
this.mScroller=new OverScroller(context);
this.mTouchSlop=ViewConfiguration.get(context).getScaledTouchSlop();
this.mMaximumVelocity=ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
this.mMinimumVelocity=ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w,h,oldw,oldh);
this.mTopViewHeight=this.mTop.getMeasuredHeight();
Log.i("lzw","topHeight"+this.mTopViewHeight);
}
/** * startScroll(滑动的起点的X,滑动的起点的Y,滑动的最终距离X,滑动的最终距离Y) * @param event * @return */
@Override
public boolean onTouchEvent(MotionEvent event){
this.mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
this.mLastY = event.getY();
if (this.mScroller.isFinished()) {
this.mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
float disY = event.getY() - this.mLastY;
if (!this.isDragging && Math.abs(disY) > this.mTouchSlop) {
this.isDragging = true;
}
if (this.isDragging) {
this.scrollBy(0, -(int) disY);
this.mLastY=event.getY();
}
break;
case MotionEvent.ACTION_UP:
this.isDragging = false;
// this.mVelocityTracker.computeCurrentVelocity(1000,this.mMaximumVelocity);
// float velocityY=this.mVelocityTracker.getYVelocity();
// //向下滑动,速率的计算值肯定是负数
// if(Math.abs(velocityY)>this.mMinimumVelocity){
// this.fling(-(int)velocityY);
// }
float disTmp=event.getY()-this.mLastY;
this.mScroller.startScroll(this.mScroller.getFinalX(),
this.getScrollY(),0,-(int)disTmp);
this.invalidate();
break;
}
return(true);
}
/** * fling函数 * startX:开始滑动的X起点 * startY:开始滚动的Y起点 * velocityX:滑动的速度X * velocityY:滑动的速度Y * minx:X方向的最小值 * maxX:X方向的最大值 * minY:Y方向的最小值 * maxY:Y方向的最大值 * @param velocity */
private void fling(int velocity){
this.mScroller.fling(0,this.getScrollY(),0,velocity,0,0,0,this.mTopViewHeight);
this.invalidate();
}
/** * 在fling或者startScroll方法后,调用invalidate方法后执行的函数 * scroller.getCurY() 返回当前Y方向的偏移 */
@Override
public void computeScroll() {
if(this.mScroller.computeScrollOffset()){
this.scrollTo(0,this.mScroller.getCurrY());
invalidate();
}
}
/** * * @param x * @param y */
@Override
public void scrollTo(int x,int y){
if(y<0) {
y = 0;
}
//指定一个滑动的上限
if(y>this.mTopViewHeight){
y=mTopViewHeight;
}
if(y!=this.getScrollY()){
super.scrollTo(x,y);
}
} @Override
protected void onFinishInflate() {
super.onFinishInflate();
this.mTop=this.findViewById(R.id.topView);
this.mBottom=this.findViewById(R.id.bottomView);
}
}
<slidingmenu.dreamfly.org.slidingmenu.custom.app.DetailRootLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<RelativeLayout
android:id="@+id/mTopView"
android:layout_width="match_parent"
android:background="#4400ff00"
android:layout_height="400dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="22sp"
android:gravity="center"
android:text="软件介绍"
/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/mBottomView"
android:layout_width="match_parent"
android:background="#4400ff00"
android:layout_height="400dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="22sp"
android:gravity="center"
android:text="软件详情"
/>
</RelativeLayout>
</slidingmenu.dreamfly.org.slidingmenu.custom.app.DetailRootLayout>
从上面的XML和java代码中,我们可以看到,自定义的LinearLayout的是一个模仿ScrollView类的自定义控件。这个LinearLayout含有两个布局:mTop和mBottom。其中,mTop布局和mBottom布局不能同时显示(mTop可以全部显示,mBottom只能显示一个部分)当我们需要手指向下滑动的时候,我们需要这个自定义的LinearLayout经过滑动后,将mTop完全隐藏,将mBottom完全显示。这时候,就需要Scroller类来辅助滑动了:我们仔细看这段代码中的这几个部分:
case MotionEvent.ACTION_UP:
this.isDragging = false;
float disTmp=event.getY()-this.mLastY;
this.mScroller.startScroll(this.mScroller.getFinalX(),
this.getScrollY(),0,-(int)disTmp);
break;
这里监听到手势抬起的动作,然后计算move的最后一点的动作y和up动作的Y的差值,然后调用Scroller的startScroll函数。这里先讲下startScroll函数的参数都是什么含义:
public void startScroll (int startX, int startY, int dx, int dy, int duration)
以提供的起始点和将要滑动的距离开始滚动。
startX 水平方向滚动的偏移值,以像素为单位。正值表明滚动将向左滚动(滑动的起点)
startY 垂直方向滚动的偏移值,以像素为单位。正值表明滚动将向上滚动(滑动的终点)
dx 水平方向滑动的距离,正值会使滚动向左滚动
dy 垂直方向滑动的距离,正值会使滚动向上滚动
duration 滚动持续时间,以毫秒计。默认是以250ms计算的
可以看到,这个函数并没有实现滑动的动作,而是记录了滑动的距离(在Scroller类的源代码中可以看到)那滑动的动作是如何实现呢?当然靠view啦,我们看接下来的代码:在startScroll之后,我们调用了 this.invalidate()方法,这个方法的意思是请求View重新绘制,这时候,view就会调用computeScroll()这个方法,这个方法在View中是空,就是等着子类来覆写这个方法。于是,我们在这里覆写方法的代码为:
@Override
public void computeScroll() {
if(this.mScroller.computeScrollOffset()){
this.scrollTo(0,this.mScroller.getCurrY());
invalidate();
}
}
这个方法里面我们使用了Scroller类的computeScrollOffset()方法,和scroller类的getCurrY()方法。这都是什么意思呢?其实很简单,在computeScrollOffset()方法中,根据startScroll方法中的被赋值的参数来进行计算现在滑动的距离,这个方法返回的是boolean数值,代表是否计算完成了,何时计算完成了呢?其实也是在startScroll中被赋值的。computeScrollOffset()不断的计算更新当前应该滑动到的curX或者curY(也就是scroller.getCurX()/getY())的数值,直到computeScrollOffset()的数值已经达到了startScroll中的指定的数值,就会返回false。
因此我们需要有这样一个不断循环的逻辑 rcomputeScroll()->getCurX/getCurY->invalidate(),来完成View的内容上的滑动/那么我们怎么控制滑动的最大间距呢?可以同通过覆写scrollTo方法:
/** * * @param x * @param y */
@Override
public void scrollTo(int x,int y){
if(y<0) {
y = 0;
}
//指定一个滑动的上限
if(y>this.mTopViewHeight){
y=mTopViewHeight;
}
if(y!=this.getScrollY()){
super.scrollTo(x,y);
}
}
来控制滑动的最大距离。
这就是,使用scroller类来实现View平缓滑动的一种方式,总结一下:View.scrollBy和View.scrollTo都是瞬时过度的,要像让View平缓过度,我们就需要利用Scroller的辅助计算,把一些复杂的过程呢个计算交给scroller,把然后配合scroller的计算结果(currY/currX)来调用View.scrollTo或者scrollBy方法。
接下来我们,看看借助scroller实现平缓滑动的其他方式(在文档中被注释掉的)那部分代码:
this.mVelocityTracker.computeCurrentVelocity(1000,this.mMaximumVelocity);
float velocityY=this.mVelocityTracker.getYVelocity(); //向下滑动,速率的计算值肯定是负数
if(Math.abs(velocityY)>this.mMinimumVelocity){
this.fling(-(int)velocityY);
可以看到,实现View的平缓滑动,可以是使用scroller.fling()方法,我们首先通过mVelocityTracker(速度追踪器类)来计算滑动的速度,然后调用scroller.fling()方法:我们来看看fling方法的参数是都代表什么意思?
* public void fling (int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)
在fling(译者注:快滑,用户按下触摸屏、快速移动后松开)手势基础上开始滚动。滚动的距离取决于fling的初速度。
参数
maxY Y方向的最大值,scroller不会滚过此点。
所以,fling方法主要也是做数据记录,记录下这些数据,之后呢?我们再次调用 invalidate();来请求重新绘制,然后和之前的逻辑是一样的了,仍然是循环逻辑,然后不断地调用View.scrollTo/By等方法来实现View的缓慢滑动。有两点与之前的方法不同的是:这里可以设置根据手势设置View滑动的速度,比较灵活一点;同时也可以不用覆写scrollTo的方法,去设置最大上限,因为在fling方法中已经设置过了。
好吧,现在我们已经了解了scroller的用法了,总结下:
Q&A:
View.getScrollY() 和 Scroller.getCurY() 的有何区别?View.getScrollY()方法代表是组件边缘和手机屏幕的距离,Scroller.getCurY()是在滑动中,下一步滑动要去的坐标的位置,两者基本没有什么太多关联