Android scrollTo() scrollBy() Scroller解說及應用
scrollTo() 、scrollBy()及 Scroller在視圖滑動中常常使用到。比方最常見的Launcher就是用這種方式實現。
為了更加明了的理解。還是去看一下源代碼。在View類中。scrollTo的代碼例如以下:
/** * Set the scrolled position of your view. This will cause a call to * [email protected] #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } }
那它是怎樣讓視圖滾動的呢?首先註意到在這種方法中有兩個變量:mScrollX、mScrollY。這兩個變量是在View類中定義的,
/** * The offset, in pixels, by which the content of this view is scrolled * horizontally. * [email protected]} */ @ViewDebug.ExportedProperty(category = "scrolling") protected int mScrollX; /** * The offset, in pixels, by which the content of this view is scrolled * vertically. * [email protected]
- mScrollX: 該視圖內容相當於視圖起始坐標的偏移量。 X軸方向
- mScrollY:該視圖內容相當於視圖起始坐標的偏移量, Y軸方向
)
scrollTo()方法就是將一個視圖移動到指定位置。偏移量 mScrollX、mScrollY就是視圖初始位置的距離,默認是情況下當然是0。假設視圖要發生移動,比方要移動到(x,y),首先要檢查這個點的坐標是否和偏移量一樣,由於 scrollTo()是移動到指定的點。假設這次移動的點的坐標和上次偏移量一樣,也就是說這次移動和上次移動的坐標是同一個,那麽就沒有必要進行移動了。這也是這種方法為什麽進行 if (mScrollX != x || mScrollY != y) {這樣一個推斷的原因。接下來再看一下scrollBy()的源代碼,/** * Move the scrolled position of your view. This will cause a call to * [email protected] #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); }非常easy。就是直接調用了scrollTo方法,可是從這種方法的實現機制能夠看出,它是一個累加減的過程,不斷的將當前視圖內容繼續偏移(x , y)個單位。
比方第一次 scrollBy(10,10)。第二次 scrollBy(10,10),那麽最後的結果就相當於scrollTo(20,20)。
理解這兩個方法的實現機制之後。另一個重要的問題,就是關於移動的方向。比方一個位於原點的視圖,假設調用了scrollTo(0,20)方法,假設你覺得是垂直向下移動20像素就錯了,事實上是向上移動了20個像素。在上圖中,我已經給出了一個十字坐標,正負代表坐標的正負以及對應的方向。為什麽會是這種情況呢?按坐標系的認知來說,不應該是這個結果的,所以必須研究一下到底為何。 線索當然還是要分析源代碼。在scrollTo(x, y)中,x和y分別被賦值給了mScrollX和mScrollY,最後調用了postInvalidateOnAnimation()方法。之後這種方法會通知View進行重繪。所以就去看一下draw()方法的源代碼,由於這種方法比較長,基於篇幅就不所有列出,直說重點。先列出方法的前幾行。
public void draw(Canvas canvas) { if (mClipBounds != null) { canvas.clipRect(mClipBounds); } final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas‘ layers to prepare for fading * 3. Draw view‘s content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed int saveCount;在凝視中能夠看到這種方法的步驟。第六步6就是繪制scrollbars,而scrollbars就是由於scroll引起的,所以先定位到這裏。
在方法的最後,看到了
// Step 6, draw decorations (scrollbars) onDrawScrollBars(canvas);然後看一下onDrawScrollBars(canvas)方法,
protected final void onDrawScrollBars(Canvas canvas) { // scrollbars are drawn only when the animation is running final ScrollabilityCache cache = mScrollCache; if (cache != null) { int state = cache.state; if (state == ScrollabilityCache.OFF) { return; } boolean invalidate = false; if (state == ScrollabilityCache.FADING) { // We‘re fading -- get our fade interpolation if (cache.interpolatorValues == null) { cache.interpolatorValues = new float[1]; } float[] values = cache.interpolatorValues; // Stops the animation if we‘re done if (cache.scrollBarInterpolator.timeToValues(values) == Interpolator.Result.FREEZE_END) { cache.state = ScrollabilityCache.OFF; } else { cache.scrollBar.setAlpha(Math.round(values[0])); } // This will make the scroll bars inval themselves after // drawing. We only want this when we‘re fading so that // we prevent excessive redraws invalidate = true; } else { // We‘re just on -- but we may have been fading before so // reset alpha cache.scrollBar.setAlpha(255); } final int viewFlags = mViewFlags; final boolean drawHorizontalScrollBar = (viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL; final boolean drawVerticalScrollBar = (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL && !isVerticalScrollBarHidden(); if (drawVerticalScrollBar || drawHorizontalScrollBar) { final int width = mRight - mLeft; final int height = mBottom - mTop; final ScrollBarDrawable scrollBar = cache.scrollBar; final int scrollX = mScrollX; final int scrollY = mScrollY; final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ?這種方法分別繪制水平和垂直方向的ScrollBar,最後都會調用invalidate(left, top, right, bottom)方法。~0 : 0; int left; int top; int right; int bottom; if (drawHorizontalScrollBar) { int size = scrollBar.getSize(false); if (size <= 0) { size = cache.scrollBarSize; } scrollBar.setParameters(computeHorizontalScrollRange(), computeHorizontalScrollOffset(), computeHorizontalScrollExtent(), false); final int verticalScrollBarGap = drawVerticalScrollBar ?
getVerticalScrollbarWidth() : 0; top = scrollY + height - size - (mUserPaddingBottom & inside); left = scrollX + (mPaddingLeft & inside); right = scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap; bottom = top + size; onDrawHorizontalScrollBar(canvas, scrollBar, left, top, right, bottom); if (invalidate) { invalidate(left, top, right, bottom); } } if (drawVerticalScrollBar) { int size = scrollBar.getSize(true); if (size <= 0) { size = cache.scrollBarSize; } scrollBar.setParameters(computeVerticalScrollRange(), computeVerticalScrollOffset(), computeVerticalScrollExtent(), true); int verticalScrollbarPosition = mVerticalScrollbarPosition; if (verticalScrollbarPosition == SCROLLBAR_POSITION_DEFAULT) { verticalScrollbarPosition = isLayoutRtl() ? SCROLLBAR_POSITION_LEFT : SCROLLBAR_POSITION_RIGHT; } switch (verticalScrollbarPosition) { default: case SCROLLBAR_POSITION_RIGHT: left = scrollX + width - size - (mUserPaddingRight & inside); break; case SCROLLBAR_POSITION_LEFT: left = scrollX + (mUserPaddingLeft & inside); break; } top = scrollY + (mPaddingTop & inside); right = left + size; bottom = scrollY + height - (mUserPaddingBottom & inside); onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom); if (invalidate) { invalidate(left, top, right, bottom); } } } } }
public void invalidate(int l, int t, int r, int b) { if (skipInvalidate()) { return; } if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED) { mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; mPrivateFlags |= PFLAG_INVALIDATED; mPrivateFlags |= PFLAG_DIRTY; final ViewParent p = mParent; final AttachInfo ai = mAttachInfo; //noinspection PointlessBooleanExpression,ConstantConditions if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } } if (p != null && ai != null && l < r && t < b) { final int scrollX = mScrollX; final int scrollY = mScrollY; final Rect tmpr = ai.mTmpInvalRect; tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY); p.invalidateChild(this, tmpr); } } }在這種方法的最後,能夠看到 tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY),真相最終大白,相信也都清楚為什麽會是反方向的了。
也會明確當向右移動視圖時候,為什麽getScrollX()返回值會是負的了。以下做一個測試的demo,來練習一下這兩個方法的使用。
Activity:package com.kince.scrolldemo; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; public class MainActivity extends Activity implements OnClickListener { private Button mButton1; private Button mButton2; private Button mButton3; private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView) this.findViewById(R.id.tv); mButton1 = (Button) this.findViewById(R.id.button_scroll1); mButton2 = (Button) this.findViewById(R.id.button_scroll2); mButton3 = (Button) this.findViewById(R.id.button_scroll3); mButton1.setOnClickListener(this); mButton2.setOnClickListener(this); mButton3.setOnClickListener(this); } @Override public void onClick(View v) { // TODO Auto-generated method stub switch (v.getId()) { case R.id.button_scroll1: mTextView.scrollTo(-10, -10); break; case R.id.button_scroll2: mTextView.scrollBy(-2, -2); break; case R.id.button_scroll3: mTextView.scrollTo(0, 0); break; default: break; } } }xml:
<LinearLayout 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:orientation="vertical" > <RelativeLayout android:layout_width="match_parent" android:layout_height="400dp" android:background="@android:color/holo_green_light" > <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:background="@android:color/holo_blue_dark" android:textSize="20sp" android:text="SCROLL" /> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="horizontal" > <Button android:id="@+id/button_scroll1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="SCROLL_TO" /> <Button android:id="@+id/button_scroll2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="SCROLL_BY" /> <Button android:id="@+id/button_scroll3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="復位" /> </LinearLayout> </LinearLayout>點擊SCROLL_TO按鈕,TxtView移動後顯演示樣例如以下: 然後,不斷按SCROLL_BY按鈕,顯演示樣例如以下: 能夠看到,TextView逐漸向下移動。直到看不到文字(還會繼續移動)。看到這種結果,可能會與之前預想的有些出入。我之前以為TextView會在它的父類容器控件中移動,也就是圖中綠黃色的區域。
結果卻是視圖相對於自身的移動,事實上還是對於這種方法包含 mScrollX、mScrollY的理解不全面,回過頭來再看一下
protected int mScrollX; //The offset, in pixels, by which the content of this view is scrolled 重點就是the content of this view。視圖的內容的偏移量。而不是視圖相對於其它容器或者視圖的偏移量。也就是說,移動的是視圖裏面的內容,從上面的樣例也能夠看出。TextView的文字移動了。而背景色一直沒變化,說明不是整個視圖在移動。 接著,改一下代碼,在xml文件裏將TextView的寬高設置成填充父容器。再看一下效果,
這下看的效果就仿佛是在父容器中移動,可是事實上還是TextView本身的內容在移動。那這兩個方法在實際開發中是怎樣運用的呢?光憑上面的樣例是看不出什麽作用的。可是就像文章開頭部分說的那樣,在視圖滑動的情況下,這兩個方法發揮了巨大的作用。以相似Launcher左右滑屏為例,
先自己定義一個View繼承於ViewGroup,例如以下:/** * */ package com.kince.scrolldemo; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; /** * @author kince * * */ public class CusScrollView extends ViewGroup { private int lastX = 0; private int currX = 0; private int offX = 0; /** * @param context */ public CusScrollView(Context context) { this(context, null); // TODO Auto-generated constructor stub } /** * @param context * @param attrs */ public CusScrollView(Context context, AttributeSet attrs) { this(context, attrs, 0); // TODO Auto-generated constructor stub } /** * @param context * @param attrs * @param defStyle */ public CusScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO Auto-generated constructor stub } /* * (non-Javadoc) * * @see android.view.ViewGroup#onLayout(boolean, int, int, int, int) */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); v.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 僅僅考慮水平方向 lastX = (int) event.getX(); return true; case MotionEvent.ACTION_MOVE: currX = (int) event.getX(); offX = currX - lastX; scrollBy(-offX, 0); break; case MotionEvent.ACTION_UP: scrollTo(0, 0); break; } invalidate(); return super.onTouchEvent(event); } }這個控件用於水平滑動裏面的視圖。Activity代碼例如以下:
package com.kince.scrolldemo; import android.app.Activity; import android.app.ActionBar; import android.app.Fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.os.Build; public class LauncherActivity extends Activity { private int[] images = { R.drawable.jy1, R.drawable.jy2, R.drawable.jy3, R.drawable.jy4, R.drawable.jy5, }; private CusScrollView mCusScrollView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_launcher); mCusScrollView = (CusScrollView) this.findViewById(R.id.CusScrollView); for (int i = 0; i < images.length; i++) { ImageView mImageView = new ImageView(this); mImageView.setScaleType(ScaleType.FIT_XY); mImageView.setBackgroundResource(images[i]); mImageView.setLayoutParams(new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); mCusScrollView.addView(mImageView); } } }在Activity中為CusScrollView加入5個ImageView用於顯示圖片,xml例如以下:
<LinearLayout 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:orientation="vertical" > <com.kince.scrolldemo.CusScrollView android:id="@+id/CusScrollView" android:layout_width="match_parent" android:layout_height="match_parent" > </com.kince.scrolldemo.CusScrollView> </LinearLayout>這個樣例對CusScrollView裏面的圖片進行左右滑動,在 onTouchEvent(MotionEvent event)的MotionEvent.ACTION_MOVE中對圖片進行移動,使用的是 scrollBy()方法,由於手指每次移動都會產生差值。利用 scrollBy()方法就能夠尾隨手指進行左右滑動。
在MotionEvent.ACTION_UP事件中,也就是手指擡起時候,直接使用scrollTo()方法讓視圖回到初始位置。再強調一遍。註意無論是scrollBy()還是scrollTo()方法,都是對CusScrollView內容視圖進行移動。效果例如以下:
至此,就大體完畢了對 scrollBy()、 scrollTo()這兩個方法的介紹。
只是通過上面的樣例,發現一個問題就是滑動速度非常快,尤其是scrollTo()方法,差點兒是瞬間移動到指定位置。這樣倒不能說是缺點。只是在某些情況下,是希望能夠緩慢的移動或者有一個明顯的移動效果。就像側滑菜單那樣。仿佛有一個移動的動畫。
這時候Scroller閃亮登場了。
Scroller類是滾動的一個封裝類,能夠實現View的平滑滾動效果,還能夠使用插值器先加速後減速,或者先減速後加速等等效果,而不是瞬間的移動的效果。那是怎樣實現帶動畫效果平滑移動的呢?除了Scroller這個類之外,還須要使用View類的computeScroll()方法來配合完畢這個過程。看一下這種方法的源代碼:/** * 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 [email protected] android.widget.Scroller Scroller} * object. */ public void computeScroll() { }從凝視中了解到當子視圖使用Scroller滑動的時候會調用這種方法,之後View類的mScrollX和mScrollY的值會對應發生變化。
而且在繪制View時,會在draw()過程調用該方法。能夠看到這種方法是一個空的方法。因此須要子類去重寫該方法來實現邏輯。那該方法在何處被觸發呢?繼續看看View的draw()方法,上面說到會在子視圖中調用該方法,也就是說繪制子視圖的時候,那麽在draw()等等的第四部。
// Step 4, draw the children dispatchDraw(canvas);正是繪制子視圖,然後看一下這種方法。
/** * Called by draw to draw the child views. This may be overridden * by derived classes to gain control just before its children are drawn * (but after its own view has been drawn). * @param canvas the canvas on which to draw the view */ protected void dispatchDraw(Canvas canvas) { }也是一個空方法,可是我們知道這種方法是ViewGroup用來繪制子視圖的方法,所以找到View的子類ViewGroup來看看該方法的詳細實現邏輯 ,基於篇幅僅僅貼部分代碼。
@Override protected void dispatchDraw(Canvas canvas) { ... ... ... if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) { for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } else { 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); } } } // Draw any disappearing views that have animations if (mDisappearingChildren != null) { final ArrayList<View> disappearingChildren = mDisappearingChildren; final int disappearingCount = disappearingChildren.size() - 1; // Go backwards -- we may delete as animations finish for (int i = disappearingCount; i >= 0; i--) { final View child = disappearingChildren.get(i); more |= drawChild(canvas, child, drawingTime); } } ... ... ... } }能夠看到。在dispatchDraw方法中調用了drawChild(canvas, child, drawingTime)方法,再看一下其代碼:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ... ... ... if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) && (child.mPrivateFlags & DRAW_ANIMATION) == 0) { return more; } child.computeScroll(); final int sx = child.mScrollX; final int sy = child.mScrollY; boolean scalingRequired = false; Bitmap cache = null; ... ... ... }果然, child.computeScroll()。在這裏調用的。
也就是ViewGroup在分發繪制自己的孩子的時候,會對其子View調用computeScroll()方法。
回過頭來再看一下Scroller,還是先看一下源代碼(簡化),public class Scroller { private int mMode; private int mStartX; private int mStartY; private int mFinalX; private int mFinalY; private int mMinX; private int mMaxX; private int mMinY; private int mMaxY; private int mCurrX; private int mCurrY; private long mStartTime; private int mDuration; private float mDurationReciprocal; private float mDeltaX; private float mDeltaY; private boolean mFinished; private Interpolator mInterpolator; private float mVelocity; private float mCurrVelocity; private int mDistance; private float mFlingFriction = ViewConfiguration.getScrollFriction(); private static final int DEFAULT_DURATION = 250; private static final int SCROLL_MODE = 0; private static final int FLING_MODE = 1; /** * Create a Scroller with the default duration and interpolator. */ public Scroller(Context context) { this(context, null); } /** * Create a Scroller with the specified interpolator. If the interpolator is * null, the default (viscous) interpolator will be used. "Flywheel" behavior will * be in effect for apps targeting Honeycomb or newer. */ public Scroller(Context context, Interpolator interpolator) { this(context, interpolator, context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); } /** * Create a Scroller with the specified interpolator. If the interpolator is * null, the default (viscous) interpolator will be used. Specify whether or * not to support progressive "flywheel" behavior in flinging. */ public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; mInterpolator = interpolator; mPpi = context.getResources().getDisplayMetrics().density * 160.0f; mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); mFlywheel = flywheel; mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning } /** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. */ public boolean computeScrollOffset() { if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: float x = timePassed * mDurationReciprocal; if (mInterpolator == null) x = viscousFluid(x); else x = mInterpolator.getInterpolation(x); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; case FLING_MODE: final float t = (float) timePassed / mDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); // Pin to mMinY <= mCurrY <= mMaxY mCurrY = Math.min(mCurrY, mMaxY); mCurrY = Math.max(mCurrY, mMinY); if (mCurrX == mFinalX && mCurrY == mFinalY) { mFinished = true; } break; } } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; } /** * Start scrolling by providing a starting point and the distance to travel. * The scroll will use the default value of 250 milliseconds for the * duration. * * @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. */ public void startScroll(int startX, int startY, int dx, int dy) { startScroll(startX, startY, dx, dy, DEFAULT_DURATION); } /** * 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; } /** * Start scrolling based on a fling gesture. The distance travelled will * depend on the initial velocity of the fling. * * @param startX Starting point of the scroll (X) * @param startY Starting point of the scroll (Y) * @param velocityX Initial velocity of the fling (X) measured in pixels per * second. * @param velocityY Initial velocity of the fling (Y) measured in pixels per * second * @param minX Minimum X value. The scroller will not scroll past this * point. * @param maxX Maximum X value. The scroller will not scroll past this * point. * @param minY Minimum Y value. The scroller will not scroll past this * point. * @param maxY Maximum Y value. The scroller will not scroll past this * point. */ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { // Continue a scroll or fling in progress if (mFlywheel && !mFinished) { float oldVel = getCurrVelocity(); float dx = (float) (mFinalX - mStartX); float dy = (float) (mFinalY - mStartY); float hyp = FloatMath.sqrt(dx * dx + dy * dy); float ndx = dx / hyp; float ndy = dy / hyp; float oldVelocityX = ndx * oldVel; float oldVelocityY = ndy * oldVel; if (Math.signum(velocityX) == Math.signum(oldVelocityX) && Math.signum(velocityY) == Math.signum(oldVelocityY)) { velocityX += oldVelocityX; velocityY += oldVelocityY; } } mMode = FLING_MODE; mFinished = false; float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); mVelocity = velocity; mDuration = getSplineFlingDuration(velocity); mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; float coeffX = velocity == 0 ?Scroller有三個構造方法,當中二、三能夠使用動畫插值器。除了構造方法外,Scroller還有以下幾個重要方法:computeScrollOffset()、startScroll(int startX, int startY, int dx, int dy, int duration)、 fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) 等。 startScroll(int startX, int startY, int dx, int dy, int duration)從方法名字來看應該是滑動開始的地方,事實上我們在使用的時候也是先調用這種方法的。它的作用是:(startX , startY)在duration時間內前進(dx,dy)個單位,即到達坐標為(startX+dx , startY+dy) 可是從源代碼來看,1.0f : velocityX / velocity; float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; double totalDistance = getSplineFlingDistance(velocity); mDistance = (int) (totalDistance * Math.signum(velocity)); mMinX = minX; mMaxX = maxX; mMinY = minY; mMaxY = maxY; mFinalX = startX + (int) Math.round(totalDistance * coeffX); // Pin to mMinX <= mFinalX <= mMaxX mFinalX = Math.min(mFinalX, mMaxX); mFinalX = Math.max(mFinalX, mMinX); mFinalY = startY + (int) Math.round(totalDistance * coeffY); // Pin to mMinY <= mFinalY <= mMaxY mFinalY = Math.min(mFinalY, mMaxY); mFinalY = Math.max(mFinalY, mMinY); } }
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; }這種方法更像是一個構造方法用來初始化賦值的,比方設置滾動模式、開始時間,持續時間、起始坐標、結束坐標等等,並沒有不論什麽對View的滾動操作。當然另一個重要的變量:mDurationReciprocal。由於這個變量要在接下來介紹的computeScrollOffset()方法使用,computeScrollOffset()方法主要是依據當前已經消逝的時間來計算當前的坐標點,而且保存在mCurrX和mCurrY值中。那這個消逝的時間就是怎樣計算出來的呢?之前在startScroll()方法的時候獲取了當前的動畫毫秒並賦值給了mStartTime。在computeScrollOffset()中再一次調用AnimationUtils.currentAnimationTimeMillis()來獲取動畫毫秒減去mStartTime就是消逝時間了。然後進去if推斷。假設動畫持續時間小於設置的滾動持續時間mDuration,則是SCROLL_MODE,再依據Interpolator來計算出在該時間段裏面移動的距離,移動的距離是依據這個消逝時間乘以mDurationReciprocal。就得到一個相對偏移量,再進行Math.round(x * mDeltaX)計算,就得到最後的偏移量。然後賦值給mCurrX, mCurrY,所以mCurrX、 mCurrY 的值也是一直變化的。總結一下該方法的作用就是,計算在0到mDuration時間段內滾動的偏移量。而且推斷滾動是否結束,true代表還沒結束,false則表示滾動結束了。 之前說到是Scroller配合computeScroll()方法來實現移動的,那是怎樣配合的呢? 1、首先調用Scroller的startScroll()方法來進行一些滾動的初始化設置,
scroller.startScroll(getScrollX(), 0, distance, 0);2、然後調用View的invalidate()或postInvalidate()進行重繪。
invalidate(); // 刷新視圖
3、繪制View的時候會觸發computeScroll()方法,接著重寫computeScroll(),在computeScroll()裏面先調用Scroller的computeScrollOffset()方法來推斷滾動是否結束。假設滾動沒有結束就調用scrollTo()方法來進行滾動。
@Override public void computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), 0); } }
4、scrollTo()方法盡管會又一次繪制View,可是還是要手動調用下invalidate()或者postInvalidate()來觸發界面重繪。又一次繪制View又觸發computeScroll(),所以就進入一個遞歸循環階段。這樣就實如今某個時間段裏面滾動某段距離的一個平滑的滾動效果。
@Override public void computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), 0); invalidate(); } }詳細流程圖例如以下:
了解完Scroller之後,我們就對之前的樣例進行一下改進。不直接使用scrollTo()、ScrollBy()方法了,而是使用Scroller來實現一個平滑的移動效果。
僅僅需把代碼略微改一下就能夠了,例如以下:
/** * */ package com.kince.scrolldemo; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Scroller; /** * @author kince * * */ public class CusScrollView extends ViewGroup { private int lastX = 0; private int currX = 0; private int offX = 0; private Scroller mScroller; /** * @param context */ public CusScrollView(Context context) { this(context, null); // TODO Auto-generated constructor stub } /** * @param context * @param attrs */ public CusScrollView(Context context, AttributeSet attrs) { this(context, attrs, 0); // TODO Auto-generated constructor stub } /** * @param context * @param attrs * @param defStyle */ public CusScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO Auto-generated constructor stub mScroller = new Scroller(context); } /* * (non-Javadoc) * * @see android.view.ViewGroup#onLayout(boolean, int, int, int, int) */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); v.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight()); } } @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 僅僅考慮水平方向 lastX = (int) event.getX(); return true; case MotionEvent.ACTION_MOVE: currX = (int) event.getX(); offX = currX - lastX; // scrollBy(-offX, 0); mScroller.startScroll(getScrollX(), 0, -offX, 0); break; case MotionEvent.ACTION_UP: // scrollTo(0, 0); mScroller.startScroll(getScrollX(), 0, -100, 0); break; } invalidate(); return super.onTouchEvent(event); } @Override public void computeScroll() { // TODO Auto-generated method stub if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), 0); invalidate(); } } }
這樣就實現了一個平滑的移動效果。關於scrollTo() 、scrollBy()、 Scroller解說就進行到這裏。之後會更新兩篇關於這方面的UI效果開發,一篇是模仿Zaker的開門效果;另一篇是首頁推薦圖片輪播效果。
Android scrollTo() scrollBy() Scroller解說及應用