Android Scroller讲解及应用


转自:Android scrollTo() scrollBy() Scroller讲解及应用



scrollTo() 、scrollBy()及 Scroller在视图滑动中经常使用到,比如最常见的Launcher就是用这种方式实现。为了更加明了的理解,还是去看一下源码。在View类中,scrollTo的代码如下:
[html]  view plain  copy
  1. /**  
  2.    * Set the scrolled position of your view. This will cause a call to  
  3.    * {@link #onScrollChanged(int, int, int, int)} and the view will be  
  4.    * invalidated.  
  5.    * @param x the x position to scroll to  
  6.    * @param y the y position to scroll to  
  7.    */  
  8.   public void scrollTo(int x, int y) {  
  9.       if (mScrollX != x || mScrollY != y) {  
  10.           int oldX = mScrollX;  
  11.           int oldY = mScrollY;  
  12.           mScrollX = x;  
  13.           mScrollY = y;  
  14.           invalidateParentCaches();  
  15.           onScrollChanged(mScrollX, mScrollY, oldX, oldY);  
  16.           if (!awakenScrollBars()) {  
  17.               postInvalidateOnAnimation();  
  18.           }  
  19.       }  
  20.   }  
 在注释中说到,该方法用于设置滚动视图的位置,然后会调用onScrollChanged(int, int, int, int)方法,最后视图会被刷新。那它是如何让视图滚动的呢?首先注意到在这个方法中有两个变量:mScrollX、mScrollY。这两个变量是在View类中定义的,
[html]  view plain  copy
  1. /**  
  2.      * The offset, in pixels, by which the content of this view is scrolled  
  3.      * horizontally.  
  4.      * {@hide}  
  5.      */  
  6.     @ViewDebug.ExportedProperty(category = "scrolling")  
  7.     protected int mScrollX;  
  8.     /**  
  9.      * The offset, in pixels, by which the content of this view is scrolled  
  10.      * vertically.  
  11.      * {@hide}  
  12.      */  
  13.     @ViewDebug.ExportedProperty(category = "scrolling")  
  14.     protected int mScrollY;  
  这两个变量分别是视图在水平和垂直方向的偏移量,
  •   mScrollX: 该视图内容相当于视图起始坐标的偏移量, X轴方向
  •   mScrollY:该视图内容相当于视图起始坐标的偏移量, Y轴方向
  分别通过getScrollX() 和getScrollY()方法获得。
  我们知道Android的坐标体系是这样的:
  
(ps:相对于父类视图的左上角坐标为坐标原点(0,0),而不是整体ViewGroup的左上角为原点。)
 
  scrollTo()方法就是将一个视图移动到指定位置,偏移量 mScrollX、mScrollY就是视图初始位置的距离,默认是情况下当然是0。如果视图要发生移动,比如要移动到(x,y),首先要检查这个点的坐标是否和偏移量一样,因为 scrollTo()是移动到指定的点,如果这次移动的点的坐标和上次偏移量一样,也就是说这次移动和上次移动的坐标是同一个,那么就没有必要进行移动了。这也是这个方法为什么进行 if (mScrollX != x || mScrollY != y) {这样一个判断的原因。接下来再看一下scrollBy()的源码,
[html]  view plain  copy
  1. /**  
  2.  * Move the scrolled position of your view. This will cause a call to  
  3.  * {@link #onScrollChanged(int, int, int, int)} and the view will be  
  4.  * invalidated.  
  5.  * @param x the amount of pixels to scroll by horizontally  
  6.  * @param y the amount of pixels to scroll by vertically  
  7.  */  
  8. public void scrollBy(int x, int y) {  
  9.     scrollTo(mScrollX + x, mScrollY + y);  
  10. }  
   很简单,就是直接调用了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()方法的源码,因为这个方法比较长,基于篇幅就不全部列出,直说重点。先列出方法的前几行,
[html]  view plain  copy
  1. public void draw(Canvas canvas) {  
  2.       if (mClipBounds != null) {  
  3.           canvas.clipRect(mClipBounds);  
  4.       }  
  5.       final int privateFlags = mPrivateFlags;  
  6.       final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&  
  7.               (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
  8.       mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;  
  9.   
  10.       /*  
  11.        * Draw traversal performs several drawing steps which must be executed  
  12.        * in the appropriate order:  
  13.        *  
  14.        *      1. Draw the background  
  15.        *      2. If necessary, save the canvas' layers to prepare for fading  
  16.        *      3. Draw view's content  
  17.        *      4. Draw children  
  18.        *      5. If necessary, draw the fading edges and restore layers  
  19.        *      6. Draw decorations (scrollbars for instance)  
  20.        */  
  21.   
  22.       // Step 1, draw the background, if needed  
  23.       int saveCount;  
  在注释中可以看到这个方法的步骤,第六步6就是绘制scrollbars,而scrollbars就是由于scroll引起的,所以先定位到这里。在方法的最后,看到了
[html]  view plain  copy
  1. // Step 6, draw decorations (scrollbars)  
  2.         onDrawScrollBars(canvas);  
   然后看一下onDrawScrollBars(canvas)方法,
[html]  view plain  copy
  1. protected final void onDrawScrollBars(Canvas canvas) {  
  2.        // scrollbars are drawn only when the animation is running  
  3.        final ScrollabilityCache cache = mScrollCache;  
  4.        if (cache != null) {  
  5.   
  6.            int state = cache.state;  
  7.   
  8.            if (state == ScrollabilityCache.OFF) {  
  9.                return;  
  10.            }  
  11.   
  12.            boolean invalidate = false;  
  13.   
  14.            if (state == ScrollabilityCache.FADING) {  
  15.                // We're fading -- get our fade interpolation  
  16.                if (cache.interpolatorValues == null) {  
  17.                    cache.interpolatorValues = new float[1];  
  18.                }  
  19.   
  20.                float[] values = cache.interpolatorValues;  
  21.   
  22.                // Stops the animation if we're done  
  23.                if (cache.scrollBarInterpolator.timeToValues(values) ==  
  24.                        Interpolator.Result.FREEZE_END) {  
  25.                    cache.state = ScrollabilityCache.OFF;  
  26.                } else {  
  27.                    cache.scrollBar.setAlpha(Math.round(values[0]));  
  28.                }  
  29.   
  30.                // This will make the scroll bars inval themselves after  
  31.                // drawing. We only want this when we're fading so that  
  32.                // we prevent excessive redraws  
  33.                invalidate = true;  
  34.            } else {  
  35.                // We're just on -- but we may have been fading before so  
  36.                // reset alpha  
  37.                cache.scrollBar.setAlpha(255);  
  38.            }  
  39.   
  40.   
  41.            final int viewFlags = mViewFlags;  
  42.   
  43.            final boolean drawHorizontalScrollBar =  
  44.                (viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;  
  45.            final boolean drawVerticalScrollBar =  
  46.                (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL  
  47.                && !isVerticalScrollBarHidden();  
  48.   
  49.            if (drawVerticalScrollBar || drawHorizontalScrollBar) {  
  50.                final int width = mRight - mLeft;  
  51.                final int height = mBottom - mTop;  
  52.   
  53.                final ScrollBarDrawable scrollBar = cache.scrollBar;  
  54.   
  55.                final int scrollX = mScrollX;  
  56.                final int scrollY = mScrollY;  
  57.                final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;  
  58.   
  59.                int left;  
  60.                int top;  
  61.                int right;  
  62.                int bottom;  
  63.   
  64.                if (drawHorizontalScrollBar) {  
  65.                    int size = scrollBar.getSize(false);  
  66.                    if (size <= 0) {  
  67.                        size = cache.scrollBarSize;  
  68.                    }  
  69.   
  70.                    scrollBar.setParameters(computeHorizontalScrollRange(),  
  71.                                            computeHorizontalScrollOffset(),  
  72.                                            computeHorizontalScrollExtent(), false);  
  73.                    final int verticalScrollBarGap = drawVerticalScrollBar ?  
  74.                            getVerticalScrollbarWidth() : 0;  
  75.                    top = scrollY + height - size - (mUserPaddingBottom & inside);  
  76.                    left = scrollX + (mPaddingLeft & inside);  
  77.                    right = scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap;  
  78.                    bottom = top + size;  
  79.                    onDrawHorizontalScrollBar(canvas, scrollBar, left, top, right, bottom);  
  80.                    if (invalidate) {  
  81.                        invalidate(left, top, right, bottom);  
  82.                    }  
  83.                }  
  84.   
  85.                if (drawVerticalScrollBar) {  
  86.                    int size = scrollBar.getSize(true);  
  87.                    if (size <= 0) {  
  88.                        size = cache.scrollBarSize;  
  89.                    }  
  90.   
  91.                    scrollBar.setParameters(computeVerticalScrollRange(),  
  92.                                            computeVerticalScrollOffset(),  
  93.                                            computeVerticalScrollExtent(), true);  
  94.                    int verticalScrollbarPosition = mVerticalScrollbarPosition;  
  95.                    if (verticalScrollbarPosition == SCROLLBAR_POSITION_DEFAULT) {  
  96.                        verticalScrollbarPosition = isLayoutRtl() ?  
  97.                                SCROLLBAR_POSITION_LEFT : SCROLLBAR_POSITION_RIGHT;  
  98.                    }  
  99.                    switch (verticalScrollbarPosition) {  
  100.                        default:  
  101.                        case SCROLLBAR_POSITION_RIGHT:  
  102.                            left = scrollX + width - size - (mUserPaddingRight & inside);  
  103.                            break;  
  104.                        case SCROLLBAR_POSITION_LEFT:  
  105.                            left = scrollX + (mUserPaddingLeft & inside);  
  106.                            break;  
  107.                    }  
  108.                    top = scrollY + (mPaddingTop & inside);  
  109.                    right = left + size;  
  110.                    bottom = scrollY + height - (mUserPaddingBottom & inside);  
  111.                    onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom);  
  112.                    if (invalidate) {  
  113.                        invalidate(left, top, right, bottom);  
  114.                    }  
  115.                }  
  116.            }  
  117.        }  
  118.    }  
  这个方法分别绘制水平和垂直方向的ScrollBar,最后都会调用invalidate(left, top, right, bottom)方法。
[html]  view plain  copy
  1. public void invalidate(int l, int t, int r, int b) {  
  2.        if (skipInvalidate()) {  
  3.            return;  
  4.        }  
  5.        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) ||  
  6.                (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID ||  
  7.                (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED) {  
  8.            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;  
  9.            mPrivateFlags |= PFLAG_INVALIDATED;  
  10.            mPrivateFlags |= PFLAG_DIRTY;  
  11.            final ViewParent p = mParent;  
  12.            final AttachInfo ai = mAttachInfo;  
  13.            //noinspection PointlessBooleanExpression,ConstantConditions  
  14.            if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {  
  15.                if (p != null && ai != null && ai.mHardwareAccelerated) {  
  16.                    // fast-track for GL-enabled applications; just invalidate the whole hierarchy  
  17.                    // with a null dirty rect, which tells the ViewAncestor to redraw everything  
  18.                    p.invalidateChild(this, null);  
  19.                    return;  
  20.                }  
  21.            }  
  22.            if (p != null && ai != null && l < r && t < b) {  
  23.                final int scrollX = mScrollX;  
  24.                final int scrollY = mScrollY;  
  25.                final Rect tmpr = ai.mTmpInvalRect;  
  26.                tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);  
  27.                p.invalidateChild(this, tmpr);  
  28.            }  
  29.        }  
  30.    }  
 在这个方法的最后,可以看到 tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY),真相终于大白,相信也都清楚为什么会是反方向的了。也会明白当向右移动视图时候,为什么getScrollX()返回值会是负的了。下面做一个测试的demo,来练习一下这两个方法的使用。
   Activity:
[html]  view plain  copy
  1. package com.kince.scrolldemo;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.view.View;  
  6. import android.view.View.OnClickListener;  
  7. import android.widget.Button;  
  8. import android.widget.TextView;  
  9.   
  10. public class MainActivity extends Activity implements OnClickListener {  
  11.   
  12.      private Button mButton1;  
  13.      private Button mButton2;  
  14.      private Button mButton3;  
  15.      private TextView mTextView;  
  16.   
  17.      @Override  
  18.      protected void onCreate(Bundle savedInstanceState) {  
  19.           super.onCreate(savedInstanceState);  
  20.           setContentView(R.layout.activity_main);  
  21.   
  22.           mTextView = (TextView) this.findViewById(R.id.tv);  
  23.   
  24.           mButton1 = (Button) this.findViewById(R.id.button_scroll1);  
  25.           mButton2 = (Button) this.findViewById(R.id.button_scroll2);  
  26.           mButton3 = (Button) this.findViewById(R.id.button_scroll3);  
  27.           mButton1.setOnClickListener(this);  
  28.           mButton2.setOnClickListener(this);  
  29.           mButton3.setOnClickListener(this);  
  30.      }  
  31.   
  32.      @Override  
  33.      public void onClick(View v) {  
  34.           // TODO Auto-generated method stub  
  35.           switch (v.getId()) {  
  36.           case R.id.button_scroll1:  
  37.                mTextView.scrollTo(-10, -10);  
  38.                break;  
  39.           case R.id.button_scroll2:  
  40.                mTextView.scrollBy(-2, -2);  
  41.                break;  
  42.           case R.id.button_scroll3:  
  43.                mTextView.scrollTo(0, 0);  
  44.                break;  
  45.           default:  
  46.                break;  
  47.           }  
  48.      }  
  49.   
  50. }  
  xml:
[html]  view plain  copy
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <RelativeLayout  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="400dp"  
  10.         android:background="@android:color/holo_green_light" >  
  11.   
  12.         <TextView  
  13.             android:id="@+id/tv"  
  14.             android:layout_width="wrap_content"  
  15.             android:layout_height="wrap_content"  
  16.             android:layout_centerInParent="true"  
  17.             android:background="@android:color/holo_blue_dark"  
  18.             android:textSize="20sp"  
  19.             android:text="SCROLL" />  
  20.     </RelativeLayout>  
  21.   
  22.     <LinearLayout  
  23.         android:layout_width="match_parent"  
  24.         android:layout_height="wrap_content"  
  25.         android:gravity="center_horizontal"  
  26.         android:orientation="horizontal" >  
  27.   
  28.         <Button  
  29.             android:id="@+id/button_scroll1"  
  30.             android:layout_width="wrap_content"  
  31.             android:layout_height="wrap_content"  
  32.             android:text="SCROLL_TO" />  
  33.   
  34.         <Button  
  35.             android:id="@+id/button_scroll2"  
  36.             android:layout_width="wrap_content"  
  37.             android:layout_height="wrap_content"  
  38.             android:text="SCROLL_BY" />  
  39.   
  40.         <Button  
  41.             android:id="@+id/button_scroll3"  
  42.             android:layout_width="wrap_content"  
  43.             android:layout_height="wrap_content"  
  44.             android:text="复位" />  
  45.     </LinearLayout>  
  46.   
  47. </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,如下:
[html]  view plain  copy
  1. /**  
  2. *  
  3. */  
  4. package com.kince.scrolldemo;  
  5.   
  6. import android.content.Context;  
  7. import android.util.AttributeSet;  
  8. import android.view.MotionEvent;  
  9. import android.view.View;  
  10. import android.view.ViewGroup;  
  11.   
  12. /**  
  13. * @author kince  
  14. *  
  15. *  
  16. */  
  17. public class CusScrollView extends ViewGroup {  
  18.   
  19.      private int lastX = 0;  
  20.      private int currX = 0;  
  21.      private int offX = 0;  
  22.   
  23.      /**  
  24.      * @param context  
  25.      */  
  26.      public CusScrollView(Context context) {  
  27.           this(context, null);  
  28.           // TODO Auto-generated constructor stub  
  29.      }  
  30.   
  31.      /**  
  32.      * @param context  
  33.      * @param attrs  
  34.      */  
  35.      public CusScrollView(Context context, AttributeSet attrs) {  
  36.           this(context, attrs, 0);  
  37.           // TODO Auto-generated constructor stub  
  38.      }  
  39.   
  40.      /**  
  41.      * @param context  
  42.      * @param attrs  
  43.      * @param defStyle  
  44.      */  
  45.      public CusScrollView(Context context, AttributeSet attrs, int defStyle) {  
  46.           super(context, attrs, defStyle);  
  47.           // TODO Auto-generated constructor stub  
  48.   
  49.      }  
  50.   
  51.      /*  
  52.      * (non-Javadoc)  
  53.      *  
  54.      * @see android.view.ViewGroup#onLayout(boolean, int, int, int, int)  
  55.      */  
  56.      @Override  
  57.      protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  58.           // TODO Auto-generated method stub  
  59.   
  60.           for (int i = 0; i < getChildCount(); i++) {  
  61.                View v = getChildAt(i);  
  62.                v.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(),  
  63.                          getHeight());  
  64.           }  
  65.      }  
  66.   
  67.      @Override  
  68.      public boolean onTouchEvent(MotionEvent event) {  
  69.           // TODO Auto-generated method stub  
  70.           switch (event.getAction()) {  
  71.           case MotionEvent.ACTION_DOWN:  
  72.                // 只考虑水平方向  
  73.                lastX = (int) event.getX();  
  74.                return true;  
  75.                 
  76.           case MotionEvent.ACTION_MOVE:  
  77.                currX = (int) event.getX();  
  78.                offX = currX - lastX;  
  79.                scrollBy(-offX, 0);  
  80.                break;  
  81.                 
  82.           case MotionEvent.ACTION_UP:  
  83.                scrollTo(0, 0);  
  84.                break;  
  85.           }  
  86.           invalidate();  
  87.           return super.onTouchEvent(event);  
  88.      }  
  89. }  
  这个控件用于水平滑动里面的视图,Activity代码如下:
[html]  view plain  copy
  1. package com.kince.scrolldemo;  
  2.   
  3. import android.app.Activity;  
  4. import android.app.ActionBar;  
  5. import android.app.Fragment;  
  6. import android.os.Bundle;  
  7. import android.view.LayoutInflater;  
  8. import android.view.Menu;  
  9. import android.view.MenuItem;  
  10. import android.view.View;  
  11. import android.view.ViewGroup;  
  12. import android.view.ViewGroup.LayoutParams;  
  13. import android.widget.ImageView;  
  14. import android.widget.ImageView.ScaleType;  
  15. import android.os.Build;  
  16.   
  17. public class LauncherActivity extends Activity {  
  18.   
  19.      private int[] images = { R.drawable.jy1, R.drawable.jy2, R.drawable.jy3,  
  20.                R.drawable.jy4, R.drawable.jy5, };  
  21.   
  22.      private CusScrollView mCusScrollView;  
  23.   
  24.      @Override  
  25.      protected void onCreate(Bundle savedInstanceState) {  
  26.           super.onCreate(savedInstanceState);  
  27.           setContentView(R.layout.activity_launcher);  
  28.   
  29.           mCusScrollView = (CusScrollView) this.findViewById(R.id.CusScrollView);  
  30.           for (int i = 0; i < images.length; i++) {  
  31.                ImageView mImageView = new ImageView(this);  
  32.                mImageView.setScaleType(ScaleType.FIT_XY);  
  33.                mImageView.setBackgroundResource(images[i]);  
  34.                mImageView.setLayoutParams(new LayoutParams(  
  35.                          LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));  
  36.                mCusScrollView.addView(mImageView);  
  37.           }  
  38.   
  39.      }  
  40.   
  41. }  
  在Activity中为CusScrollView添加5个ImageView用于显示图片,xml如下:
[html]  view plain  copy
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.kince.scrolldemo.CusScrollView  
  8.         android:id="@+id/CusScrollView"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="match_parent" >  
  11.          
  12.     </com.kince.scrolldemo.CusScrollView>  
  13.   
  14. </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()方法来配合完成这个过程。看一下这个方法的源码:
[html]  view plain  copy
  1. /**  
  2.     * Called by a parent to request that a child update its values for mScrollX  
  3.     * and mScrollY if necessary. This will typically be done if the child is  
  4.     * animating a scroll using a {@link android.widget.Scroller Scroller}  
  5.     * object.  
  6.     */  
  7.    public void computeScroll() {  
  8.    }  
  从注释中了解到当子视图使用Scroller滑动的时候会调用这个方法,之后View类的mScrollX和mScrollY的值会相应发生变化。并且在绘制View时,会在draw()过程调用该方法。可以看到这个方法是一个空的方法,因此需要子类去重写该方法来实现逻辑,那该方法在何处被触发呢?继续看看View的draw()方法,上面说到会在子视图中调用该方法,也就是说绘制子视图的时候,那么在draw()等等的第四部,
[html]  view plain  copy
  1. // Step 4, draw the children   
  2.           dispatchDraw(canvas);   
  正是绘制子视图,然后看一下这个方法,
[html]  view plain  copy
  1. /**  
  2.     * Called by draw to draw the child views. This may be overridden  
  3.     * by derived classes to gain control just before its children are drawn  
  4.     * (but after its own view has been drawn).  
  5.     * @param canvas the canvas on which to draw the view  
  6.     */   
  7.    protected void dispatchDraw(Canvas canvas) {   
  8.    
  9.    }  
 也是一个空方法,但是我们知道这个方法是ViewGroup用来绘制子视图的方法,所以找到View的子类ViewGroup来看看该方法的具体实现逻辑 ,基于篇幅只贴部分代码。
[html]  view plain  copy
  1. @Override  
  2.   protected void dispatchDraw(Canvas canvas) {  
  3.       ...  
  4.       ...  
  5.       ...  
  6.   
  7.       if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {  
  8.           for (int i = 0; i < count; i++) {  
  9.               final View child = children[i];  
  10.               if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
  11.                   more |= drawChild(canvas, child, drawingTime);  
  12.               }  
  13.           }  
  14.       } else {  
  15.           for (int i = 0; i < count; i++) {  
  16.               final View child = children[getChildDrawingOrder(count, i)];  
  17.               if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
  18.                   more |= drawChild(canvas, child, drawingTime);  
  19.               }  
  20.           }  
  21.       }  
  22.   
  23.       // Draw any disappearing views that have animations  
  24.       if (mDisappearingChildren != null) {  
  25.           final ArrayList<View> disappearingChildren = mDisappearingChildren;  
  26.           final int disappearingCount = disappearingChildren.size() - 1;  
  27.           // Go backwards -- we may delete as animations finish  
  28.           for (int i = disappearingCount; i >= 0; i--) {  
  29.               final View child = disappearingChildren.get(i);  
  30.               more |= drawChild(canvas, child, drawingTime);  
  31.           }  
  32.       }  
  33.       ...  
  34.       ...  
  35.       ...  
  36.   
  37.       }  
  38.   }  
  可以看到,在dispatchDraw方法中调用了drawChild(canvas, child, drawingTime)方法,再看一下其代码:
[html]  view plain  copy
  1. protected boolean drawChild(Canvas canvas, View child, long drawingTime) {  
  2.         ...  
  3.         ...  
  4.         ...  
  5.   
  6.     if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&  
  7.                 (child.mPrivateFlags & DRAW_ANIMATION) == 0) {  
  8.             return more;  
  9.         }  
  10.   
  11.         child.computeScroll();  
  12.   
  13.         final int sx = child.mScrollX;  
  14.         final int sy = child.mScrollY;  
  15.   
  16.         boolean scalingRequired = false;  
  17.         Bitmap cache = null;  
  18.   
  19.         ...  
  20.         ...  
  21.         ...  
  22.   
  23. }  
  果然, child.computeScroll(),在这里调用的。也就是ViewGroup在分发绘制自己的孩子的时候,会对其子View调用computeScroll()方法。
   回过头来再看一下Scroller,还是先看一下源码(简化),

[html]  view plain  copy
  1. public class Scroller  {  
  2.     private int mMode;  
  3.   
  4.     private int mStartX;  
  5.     private int mStartY;  
  6.     private int mFinalX;  
  7.     private int mFinalY;  
  8.   
  9.     private int mMinX;  
  10.     private int mMaxX;  
  11.     private int mMinY;  
  12.     private int mMaxY;  
  13.   
  14.     private int mCurrX;  
  15.     private int mCurrY;  
  16.     private long mStartTime;  
  17.     private int mDuration;  
  18.     private float mDurationReciprocal;  
  19.     private float mDeltaX;  
  20.     private float mDeltaY;  
  21.     private boolean mFinished;  
  22.     private Interpolator mInterpolator;  
  23.   
  24.     private float mVelocity;  
  25.     private float mCurrVelocity;  
  26.     private int mDistance;  
  27.   
  28.     private float mFlingFriction = ViewConfiguration.getScrollFriction();  
  29.   
  30.     private static final int DEFAULT_DURATION = 250;  
  31.     private static final int SCROLL_MODE = 0;  
  32.     private static final int FLING_MODE = 1;  
  33.   
  34.     /**  
  35.      * Create a Scroller with the default duration and interpolator.  
  36.      */  
  37.     public Scroller(Context context) {  
  38.         this(context, null);  
  39.     }  
  40.   
  41.     /**  
  42.      * Create a Scroller with the specified interpolator. If the interpolator is  
  43.      * null, the default (viscous) interpolator will be used. "Flywheel" behavior will  
  44.      * be in effect for apps targeting Honeycomb or newer.  
  45.      */  
  46.     public Scroller(Context context, Interpolator interpolator) {  
  47.         this(context, interpolator,  
  48.                 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);  
  49.     }  
  50.   
  51.     /**  
  52.      * Create a Scroller with the specified interpolator. If the interpolator is  
  53.      * null, the default (viscous) interpolator will be used. Specify whether or  
  54.      * not to support progressive "flywheel" behavior in flinging.  
  55.      */  
  56.     public Scroller(Context context, Interpolator interpolator, boolean flywheel) {  
  57.         mFinished = true;  
  58.         mInterpolator = interpolator;  
  59.         mPpi = context.getResources().getDisplayMetrics().density * 160.0f;  
  60.         mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());  
  61.         mFlywheel = flywheel;  
  62.   
  63.         mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning  
  64.     }  
  65.   
  66.     /**  
  67.      * Call this when you want to know the new location.  If it returns true,  
  68.      * the animation is not yet finished.  
  69.      */  
  70.     public boolean computeScrollOffset() {  
  71.         if (mFinished) {  
  72.             return false;  
  73.         }  
  74.   
  75.         int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);  
  76.      
  77.         if (timePassed < mDuration) {  
  78.             switch (mMode) {  
  79.             case SCROLL_MODE:  
  80.                 float x = timePassed * mDurationReciprocal;  
  81.      
  82.                 if (mInterpolator == null)  
  83.                     x = viscousFluid(x);  
  84.                 else  
  85.                     x = mInterpolator.getInterpolation(x);  
  86.      
  87.                 mCurrX = mStartX + Math.round(x * mDeltaX);  
  88.                 mCurrY = mStartY + Math.round(x * mDeltaY);  
  89.                 break;  
  90.             case FLING_MODE:  
  91.                 final float t = (float) timePassed / mDuration;  
  92.                 final int index = (int) (NB_SAMPLES * t);  
  93.                 float distanceCoef = 1.f;  
  94.                 float velocityCoef = 0.f;  
  95.                 if (index < NB_SAMPLES) {  
  96.                     final float t_inf = (float) index / NB_SAMPLES;  
  97.                     final float t_sup = (float) (index + 1) / NB_SAMPLES;  
  98.                     final float d_inf = SPLINE_POSITION[index];  
  99.                     final float d_sup = SPLINE_POSITION[index + 1];  
  100.                     velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);  
  101.                     distanceCoef = d_inf + (t - t_inf) * velocityCoef;  
  102.                 }  
  103.   
  104.                 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;  
  105.                  
  106.                 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));  
  107.                 // Pin to mMinX <= mCurrX <= mMaxX  
  108.                 mCurrX = Math.min(mCurrX, mMaxX);  
  109.                 mCurrX = Math.max(mCurrX, mMinX);  
  110.                  
  111.                 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));  
  112.                 // Pin to mMinY <= mCurrY <= mMaxY  
  113.                 mCurrY = Math.min(mCurrY, mMaxY);  
  114.                 mCurrY = Math.max(mCurrY, mMinY);  
  115.   
  116.                 if (mCurrX == mFinalX && mCurrY == mFinalY) {  
  117.                     mFinished = true;  
  118.                 }  
  119.   
  120.                 break;  
  121.             }  
  122.         }  
  123.         else {  
  124.             mCurrX = mFinalX;  
  125.             mCurrY = mFinalY;  
  126.             mFinished = true;  
  127.         }  
  128.         return true;  
  129.     }  
  130.      
  131.     /**  
  132.      * Start scrolling by providing a starting point and the distance to travel.  
  133.      * The scroll will use the default value of 250 milliseconds for the  
  134.      * duration.  
  135.      *  
  136.      * @param startX Starting horizontal scroll offset in pixels. Positive  
  137.      *        numbers will scroll the content to the left.  
  138.      * @param startY Starting vertical scroll offset in pixels. Positive numbers  
  139.      *        will scroll the content up.  
  140.      * @param dx Horizontal distance to travel. Positive numbers will scroll the  
  141.      *        content to the left.  
  142.      * @param dy Vertical distance to travel. Positive numbers will scroll the  
  143.      *        content up.  
  144.      */  
  145.     public void startScroll(int startX, int startY, int dx, int dy) {  
  146.         startScroll(startX, startY, dx, dy, DEFAULT_DURATION);  
  147.     }  
  148.   
  149.     /**  
  150.      * Start scrolling by providing a starting point, the distance to travel,  
  151.      * and the duration of the scroll.  
  152.      *  
  153.      * @param startX Starting horizontal scroll offset in pixels. Positive  
  154.      *        numbers will scroll the content to the left.  
  155.      * @param startY Starting vertical scroll offset in pixels. Positive numbers  
  156.      *        will scroll the content up.  
  157.      * @param dx Horizontal distance to travel. Positive numbers will scroll the  
  158.      *        content to the left.  
  159.      * @param dy Vertical distance to travel. Positive numbers will scroll the  
  160.      *        content up.  
  161.      * @param duration Duration of the scroll in milliseconds.  
  162.      */  
  163.     public void startScroll(int startX, int startY, int dx, int dy, int duration) {  
  164.         mMode = SCROLL_MODE;  
  165.         mFinished = false;  
  166.         mDuration = duration;  
  167.         mStartTime = AnimationUtils.currentAnimationTimeMillis();  
  168.         mStartX = startX;  
  169.         mStartY = startY;  
  170.         mFinalX = startX + dx;  
  171.         mFinalY = startY + dy;  
  172.         mDeltaX = dx;  
  173.         mDeltaY = dy;  
  174.         mDurationReciprocal = 1.0f / (float) mDuration;  
  175.     }  
  176.   
  177.     /**  
  178.      * Start scrolling based on a fling gesture. The distance travelled will  
  179.      * depend on the initial velocity of the fling.  
  180.      *  
  181.      * @param startX Starting point of the scroll (X)  
  182.      * @param startY Starting point of the scroll (Y)  
  183.      * @param velocityX Initial velocity of the fling (X) measured in pixels per  
  184.      *        second.  
  185.      * @param velocityY Initial velocity of the fling (Y) measured in pixels per  
  186.      *        second  
  187.      * @param minX Minimum X value. The scroller will not scroll past this  
  188.      *        point.  
  189.      * @param maxX Maximum X value. The scroller will not scroll past this  
  190.      *        point.  
  191.      * @param minY Minimum Y value. The scroller will not scroll past this  
  192.      *        point.  
  193.      * @param maxY Maximum Y value. The scroller will not scroll past this  
  194.      *        point.  
  195.      */  
  196.     public void fling(int startX, int startY, int velocityX, int velocityY,  
  197.             int minX, int maxX, int minY, int maxY) {  
  198.         // Continue a scroll or fling in progress  
  199.         if (mFlywheel && !mFinished) {  
  200.             float oldVel = getCurrVelocity();  
  201.   
  202.             float dx = (float) (mFinalX - mStartX);  
  203.             float dy = (float) (mFinalY - mStartY);  
  204.             float hyp = FloatMath.sqrt(dx * dx + dy * dy);  
  205.   
  206.             float ndx = dx / hyp;  
  207.             float ndy = dy / hyp;  
  208.   
  209.             float oldVelocityX = ndx * oldVel;  
  210.             float oldVelocityY = ndy * oldVel;  
  211.             if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&  
  212.                     Math.signum(velocityY) == Math.signum(oldVelocityY)) {  
  213.                 velocityX += oldVelocityX;  
  214.                 velocityY += oldVelocityY;  
  215.             }  
  216.         }  
  217.   
  218.         mMode = FLING_MODE;  
  219.         mFinished = false;  
  220.   
  221.         float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);  
  222.       
  223.         mVelocity = velocity;  
  224.         mDuration = getSplineFlingDuration(velocity);  
  225.         mStartTime = AnimationUtils.currentAnimationTimeMillis();  
  226.         mStartX = startX;  
  227.         mStartY = startY;  
  228.   
  229.         float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;  
  230.         float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;  
  231.   
  232.         double totalDistance = getSplineFlingDistance(velocity);  
  233.         mDistance = (int) (totalDistance * Math.signum(velocity));  
  234.          
  235.         mMinX = minX;  
  236.         mMaxX = maxX;  
  237.         mMinY = minY;  
  238.         mMaxY = maxY;  
  239.   
  240.         mFinalX = startX + (int) Math.round(totalDistance * coeffX);  
  241.         // Pin to mMinX <= mFinalX <= mMaxX  
  242.         mFinalX = Math.min(mFinalX, mMaxX);  
  243.         mFinalX = Math.max(mFinalX, mMinX);  
  244.          
  245.         mFinalY = startY + (int) Math.round(totalDistance * coeffY);  
  246.         // Pin to mMinY <= mFinalY <= mMaxY  
  247.         mFinalY = Math.min(mFinalY, mMaxY);  
  248.         mFinalY = Math.max(mFinalY, mMinY);  
  249.     }  
  250.      
  251. }  
  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)    但是从源码来看,
[html]  view plain  copy
  1. public void startScroll(int startX, int startY, int dx, int dy, int duration) {  
  2.       mMode = SCROLL_MODE;  
  3.       mFinished = false;  
  4.       mDuration = duration;  
  5.       mStartTime = AnimationUtils.currentAnimationTimeMillis();  
  6.       mStartX = startX;  
  7.       mStartY = startY;  
  8.       mFinalX = startX + dx;  
  9.       mFinalY = startY + dy;  
  10.       mDeltaX = dx;  
  11.       mDeltaY = dy;  
  12.       mDurationReciprocal = 1.0f / (float) mDuration;  
  13.   }  
  这个方法更像是一个构造方法用来初始化赋值的,比如设置滚动模式、开始时间,持续时间、起始坐标、结束坐标等等,并没有任何对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()方法来进行一些滚动的初始化设置,
[html]  view plain  copy
  1. scroller.startScroll(getScrollX(), 0, distance, 0);  
  
  2、然后调用View的invalidate()或postInvalidate()进行重绘。
  
[html]  view plain  copy
  1. invalidate(); // 刷新视图   

  3、绘制View的时候会触发computeScroll()方法,接着重写computeScroll(),在computeScroll()里面先调用Scroller的computeScrollOffset()方法来判断滚动是否结束,如果滚动没有结束就调用scrollTo()方法来进行滚动。
[html]  view plain  copy
  1. @Override  
  2. blic void computeScroll() {  
  3.    if (scroller.computeScrollOffset()) {  
  4.         scrollTo(scroller.getCurrX(), 0);  
  5.    }  

  4、scrollTo()方法虽然会重新绘制View,但是还是要手动调用下invalidate()或者postInvalidate()来触发界面重绘,重新绘制View又触发computeScroll(),所以就进入一个递归循环阶段,这样就实现在某个时间段里面滚动某段距离的一个平滑的滚动效果。
[html]  view plain  copy
  1.   @Override  
  2. public void computeScroll() {  
  3.      if (scroller.computeScrollOffset()) {  
  4.           scrollTo(scroller.getCurrX(), 0);  
  5.           invalidate();  
  6.      }  
  7. }  
   具体流程图如下:


  了解完Scroller之后,我们就对之前的例子进行一下改进,不直接使用scrollTo()、ScrollBy()方法了,而是使用Scroller来实现一个平滑的移动效果。只需把代码稍微改一下就可以了,如下:
[html]  view plain  copy
  1. /**  
  2. *  
  3. */  
  4. package com.kince.scrolldemo;  
  5.   
  6. import android.content.Context;  
  7. import android.util.AttributeSet;  
  8. import android.view.MotionEvent;  
  9. import android.view.View;  
  10. import android.view.ViewGroup;  
  11. import android.widget.Scroller;  
  12.   
  13. /**  
  14. * @author kince  
  15. *  
  16. *  
  17. */  
  18. public class CusScrollView extends ViewGroup {  
  19.   
  20.      private int lastX = 0;  
  21.      private int currX = 0;  
  22.      private int offX = 0;  
  23.      private Scroller mScroller;  
  24.   
  25.      /**  
  26.      * @param context  
  27.      */  
  28.      public CusScrollView(Context context) {  
  29.           this(context, null);  
  30.           // TODO Auto-generated constructor stub  
  31.      }  
  32.   
  33.      /**  
  34.      * @param context  
  35.      * @param attrs  
  36.      */  
  37.      public CusScrollView(Context context, AttributeSet attrs) {  
  38.           this(context, attrs, 0);  
  39.           // TODO Auto-generated constructor stub  
  40.      }  
  41.   
  42.      /**  
  43.      * @param context  
  44.      * @param attrs  
  45.      * @param defStyle  
  46.      */  
  47.      public CusScrollView(Context context, AttributeSet attrs, int defStyle) {  
  48.           super(context, attrs, defStyle);  
  49.           // TODO Auto-generated constructor stub  
  50.           mScroller = new Scroller(context);  
  51.      }  
  52.   
  53.      /*  
  54.      * (non-Javadoc)  
  55.      *  
  56.      * @see android.view.ViewGroup#onLayout(boolean, int, int, int, int)  
  57.      */  
  58.      @Override  
  59.      protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  60.           // TODO Auto-generated method stub  
  61.   
  62.           for (int i = 0; i < getChildCount(); i++) {  
  63.                View v = getChildAt(i);  
  64.                v.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(),  
  65.                          getHeight());  
  66.           }  
  67.      }  
  68.   
  69.      @Override  
  70.      public boolean onTouchEvent(MotionEvent event) {  
  71.           // TODO Auto-generated method stub  
  72.           switch (event.getAction()) {  
  73.           case MotionEvent.ACTION_DOWN:  
  74.                // 只考虑水平方向  
  75.                lastX = (int) event.getX();  
  76.                return true;  
  77.   
  78.           case MotionEvent.ACTION_MOVE:  
  79.                currX = (int) event.getX();  
  80.                offX = currX - lastX;  
  81.                // scrollBy(-offX, 0);  
  82.                mScroller.startScroll(getScrollX(), 0, -offX, 0);  
  83.   
  84.                break;  
  85.   
  86.           case MotionEvent.ACTION_UP:  
  87. //               scrollTo(0, 0);  
  88.                mScroller.startScroll(getScrollX(), 0, -100, 0);  
  89.                break;  
  90.           }  
  91.           invalidate();  
  92.           return super.onTouchEvent(event);  
  93.      }  
  94.   
  95.      @Override  
  96.      public void computeScroll() {  
  97.           // TODO Auto-generated method stub  
  98.           if (mScroller.computeScrollOffset()) {  
  99.                scrollTo(mScroller.getCurrX(), 0);  
  100.                invalidate();  
  101.           }  
  102.      }  
  103.   
  104. }  


  这样就实现了一个平滑的移动效果。关于scrollTo() 、scrollBy()、 Scroller讲解就进行到这里。之后会更新两篇关于这方面的UI效果开发,一篇是模仿Zaker的开门效果;另一篇是首页推荐图片轮播效果。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值