这两个方法的主要作用是将View/ViewGroup移至指定的坐标中,并且将偏移量保存起来。另外:
mScrollX 代表X轴方向的偏移坐标
mScrollY 代表Y轴方向的偏移坐标
这两个方法都是用于对View进行滚动的,那么它们之间有什么区别呢?简单点讲,scrollBy()方法是让View相对于当前的位置滚动某段距离,而scrollTo()方法则是让View相对于初始的位置滚动某段距离。
关于偏移量的设置我们可以参看下源码:
public class View { .... protected int mScrollX; //该视图内容相当于视图起始坐标的偏移量,X轴方向 protected int mScrollY; //该视图内容相当于视图起始坐标的偏移量,Y轴方向 //返回值 public final int getScrollX() { return mScrollX; } public final int getScrollY() { return mScrollY; } public void scrollTo(int x, int y) { //偏移位置发生了改变 if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; //赋新值,保存当前便宜量 mScrollY = y; //回调onScrollChanged方法 onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { invalidate(); //一般都引起重绘 } } } // 看出区别了吧 。 mScrollX 与 mScrollY 代表我们当前偏移的位置 , 在当前位置继续偏移(x ,y)个单位 public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); } //... }于是,在任何时刻我们都可以获取该View/ViewGroup的偏移位置了,即调用getScrollX()方法和getScrollY()方法。
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/layout"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><Buttonandroid:id="@+id/scroll_to_btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="scrollTo"/><Buttonandroid:id="@+id/scroll_by_btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:text="scrollBy"/></LinearLayout>外层使用了一个LinearLayout,在里面包含了两个按钮,一个用于触发scrollTo逻辑,一个用于触发scrollBy逻辑。
public class MainActivity extends AppCompatActivity {private LinearLayout layout;private Button scrollToBtn;private Button scrollByBtn;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);layout = (LinearLayout) findViewById(R.id.layout);scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);scrollToBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {layout.scrollTo(getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll),getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll));}});scrollByBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {layout.scrollBy(getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll),getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll));}});}}<resources><dimen name="horizontal_scroll">-20dp</dimen><dimen name="vertical_scroll">-30dp</dimen></resources>当点击了scrollTo按钮时,我们调用了LinearLayout的scrollTo()方法,当点击了scrollBy按钮时,调用了LinearLayout的scrollBy()方法。那有的朋友可能会问了,为什么都是调用的LinearLayout中的scroll方法?这里一定要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,而LinearLayout中的内容就是我们的两个Button,如果你直接调用button的scroll方法的话,那结果一定不是你想看到的。
public class Scroller { private int mStartX;//起始坐标点 , X轴方向 private int mStartY;//起始坐标点 , Y轴方向 private int mCurrX; //当前坐标点 X轴, 即调用startScroll函数后,经过一定时间所达到的值 private int mCurrY; //当前坐标点 Y轴, 即调用startScroll函数后,经过一定时间所达到的值 private float mDeltaX; //应该继续滑动的距离, X轴方向 private float mDeltaY; //应该继续滑动的距离, Y轴方向 private boolean mFinished; //是否已经完成本次滑动操作, 如果完成则为 true //构造函数 public Scroller(Context context) { this(context, null); } public final boolean isFinished() { return mFinished; } //强制结束本次滑屏操作 public final void forceFinished(boolean finished) { mFinished = finished; } public final int getCurrX() { return mCurrX; }/* Call this when you want to know the new location. If it returns true,* the animation is not yet finished. loc will be altered to provide the* new location. *///根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中 public boolean computeScrollOffset() { if (mFinished) { //已经完成了本次动画控制,直接返回为false return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: float x = (float)timePassed * mDurationReciprocal; ... mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; ... } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; } //开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx , startY+dy)出 public void startScroll(int startX, int startY, int dx, int dy, int duration) { mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX;mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx;mDeltaY = dy; ... } }其中比较重要的两个方法为:
/*** Called by a parent to request that a child update its values for mScrollX and mScrollY if necessary. This will typically be done if the child is animating a scroll using a {@link android.widget.Scroller Scroller}* object.* 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制 */public void computeScroll() { //空方法 ,自定义ViewGroup必须实现方法体 }为了实现偏移控制,一般自定义View/ViewGroup都需要重载该方法 。其调用过程位于View绘制流程draw()过程中,如下:
@Override protected void dispatchDraw(Canvas canvas){ ... for (int i = 0; i < count; i++) { final View child = children[getChildDrawingOrder(count, i)]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ... child.computeScroll(); ... }实例演示
public class ScrollerLayout extends ViewGroup {private Scroller mScroller; //用于完成滚动操作的实例private VelocityTracker mVelocityTracker = null ; //处理触摸的速率public static int SNAP_VELOCITY = 600 ; //最小的滑动速率private int mTouchSlop = 0 ;//最小滑动距离,超过了,才认为开始滑动private float mLastionMotionX = 0 ;//上次触发ACTION_MOVE事件时的屏幕坐标private int curScreen = 0 ; //当前屏幕private int leftBorder;//界面可滚动的左边界private int rightBorder; //界面可滚动的右边界//两种状态: 是否处于滑屏状态private static final int TOUCH_STATE_REST = 0; //什么都没做的状态private static final int TOUCH_STATE_SCROLLING = 1; //开始滑屏的状态private int mTouchState = TOUCH_STATE_REST; //默认是什么都没做的状态public ScrollerLayout(Context context, AttributeSet attrs) {super(context, attrs);// 创建Scroller的实例mScroller = new Scroller(context);//初始化一个最小滑动距离mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int childCount = getChildCount();for (int i = 0; i < childCount; i++) {View childView = getChildAt(i);// 为ScrollerLayout中的每一个子控件测量大小measureChild(childView, widthMeasureSpec, heightMeasureSpec);}}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {if (changed) {int childCount = getChildCount();for (int i = 0; i < childCount; i++) {View childView = getChildAt(i);// 为ScrollerLayout中的每一个子控件在水平方向上进行布局childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());}}// 初始化左右边界值leftBorder = getChildAt(0).getLeft();rightBorder = getChildAt(getChildCount() - 1).getRight();}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {final int action = ev.getAction();//表示已经开始滑动了,不需要走该Action_MOVE方法了(第一次时可能调用)。//该方法主要用于用户快速松开手指,又快速按下的行为。此时认为是处于滑屏状态的。if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {return true;}final float x = ev.getX();switch (action) {case MotionEvent.ACTION_MOVE:final int xDiff = (int) Math.abs(mLastionMotionX - x);//超过了最小滑动距离,就可以认为开始滑动了if (xDiff > mTouchSlop) {mTouchState = TOUCH_STATE_SCROLLING;}break;case MotionEvent.ACTION_DOWN:mLastionMotionX = x;mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:mTouchState = TOUCH_STATE_REST;break;}return mTouchState != TOUCH_STATE_REST;}public boolean onTouchEvent(MotionEvent event){super.onTouchEvent(event);//获得VelocityTracker对象,并且添加滑动对象if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}mVelocityTracker.addMovement(event);//触摸点float x = event.getX();switch(event.getAction()){case MotionEvent.ACTION_DOWN://如果屏幕的动画还没结束,你就按下了,我们就结束上一次动画,即开始这次新ACTION_DOWN的动画if(mScroller != null){if(!mScroller.isFinished()){mScroller.abortAnimation();}}mLastionMotionX = x ; //记住开始落下的屏幕点break ;case MotionEvent.ACTION_MOVE:int detaX = (int)(mLastionMotionX - x ); //每次滑动屏幕,屏幕应该移动的距离if (getScrollX() + detaX < leftBorder) {//防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用scrollTo()方法来回到边界位置scrollTo(leftBorder, 0);return true;} else if (getScrollX() + getWidth() + detaX > rightBorder) {scrollTo(rightBorder - getWidth(), 0);return true;}scrollBy(detaX, 0);//开始缓慢滑屏咯。 detaX > 0 向右滑动 , detaX < 0 向左滑动mLastionMotionX = x ;break ;case MotionEvent.ACTION_UP:final VelocityTracker velocityTracker = mVelocityTracker ;velocityTracker.computeCurrentVelocity(1000);//计算速率int velocityX = (int) velocityTracker.getXVelocity() ;//滑动速率达到了一个标准(快速向右滑屏,返回上一个屏幕) 马上进行切屏处理if (velocityX > SNAP_VELOCITY && curScreen > 0) {// Fling enough to move leftsnapToScreen(curScreen - 1);}//快速向左滑屏,返回下一个屏幕else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){snapToScreen(curScreen + 1);}//以上为快速移动的 ,强制切换屏幕else{//我们是缓慢移动的,因此先判断是保留在本屏幕还是到下一屏幕snapToDestination();}//回收VelocityTracker对象if (mVelocityTracker != null) {mVelocityTracker.recycle();mVelocityTracker = null;}//修正mTouchState值mTouchState = TOUCH_STATE_REST ;break;case MotionEvent.ACTION_CANCEL:mTouchState = TOUCH_STATE_REST ;break;}return true ;}//我们是缓慢移动的,因此需要根据偏移值判断目标屏是哪个private void snapToDestination(){//判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕//公式意思是:假设当前滑屏偏移值即 scrollCurX 加上每个屏幕一半的宽度,除以每个屏幕的宽度就是我们目标屏所在位置了。int destScreen = (getScrollX() + getWidth() / 2 ) / getWidth() ;snapToScreen(destScreen);}//真正的实现跳转屏幕的方法private void snapToScreen(int whichScreen){//简单的移到目标屏幕,可能是当前屏或者下一屏幕,直接跳转过去,不太友好,为了友好性,我们在增加一个动画效果curScreen = whichScreen ;//防止屏幕越界,即超过屏幕数if(curScreen > getChildCount() - 1)curScreen = getChildCount() - 1 ;//为了达到下一屏幕或者当前屏幕,我们需要继续滑动的距离.根据dx值,可能向左滑动,也可能向右滑动int dx = curScreen * getWidth() - getScrollX() ;mScroller.startScroll(getScrollX(), 0, dx, 0, Math.abs(dx) * 2);//由于触摸事件不会重新绘制View,所以此时需要手动刷新View 否则没效果invalidate();}@Overridepublic void computeScroll() {//重写computeScroll()方法,并在其内部完成平滑滚动的逻辑if (mScroller.computeScrollOffset()) {scrollTo(mScroller.getCurrX(), mScroller.getCurrY());invalidate();}}}代码比较长,但思路比较清晰。
<?xml version="1.0" encoding="utf-8"?><com.hx.scroller.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:layout_width="match_parent"android:layout_height="200dp"android:background="@drawable/crazy_1" /><ImageViewandroid:layout_width="match_parent"android:layout_height="200dp"android:background="@drawable/crazy_2" /><ImageViewandroid:layout_width="match_parent"android:layout_height="200dp"android:background="@drawable/crazy_3" /></com.hx.scroller.ScrollerLayout>以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。