您的位置:首页 > 移动开发 > Android开发

android View的scrollTo,scrollBy方法详解

2016-05-29 23:32 561 查看
今天写一篇关于view滑动的基础技术点,首先讲下所有滑动应该都是基于View本身的scrollTo(),scrollBy(),像Scroller,ViewDrawHelper类都是基于它的封装,现在写例子看看2个方法怎么用的,

<RelativeLayout 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"

    >

    <LinearLayout

        android:id="@+id/ll_root"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:background="#ff0000"

        android:orientation="horizontal"

         />

</RelativeLayout>

布局中就一个LinearLayout,里面什么内容都没有,

public class MainActivity extends Activity {
private LinearLayout ll_root;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ll_root = (LinearLayout) findViewById(R.id.ll_root);
ll_root.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "scrollTo方法演示", 1).show();
ll_root.scrollTo(100, 0);
}
});
}

}



发现点击时候调用了scrollTo()方法并没有滑动,现在我在LienarLayout布局中添加2个子view,

<RelativeLayout 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"

    >

    <LinearLayout

        android:id="@+id/ll_root"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:background="#ff0000"

        android:orientation="horizontal"

        android:gravity="center"

         >

         <TextView 

             android:layout_width="0dp"

             android:layout_height="60dp"

             android:text="item11111"

              android:layout_weight="1"

              android:gravity="center"

              android:background="#ff00ff"

             />

          <TextView 

             android:layout_width="0dp"

             android:layout_height="60dp"

             android:text="item22222"

             android:layout_weight="1"

             android:gravity="center"

             android:background="#00ffff"

             />

         </LinearLayout>

</RelativeLayout>

MainActivity类中的代码不变,效果:



现在发现点击LinearLayout,里面的textview位置有变化了,这样就得出一个很重要的结论:

当一个view调用scrollTo()时,是里面的内容进行滑动,不是本身view滑动!

现在讨论为什么调用了scrollTo(100,0)它是x轴方法平移了100px,为什么是向左偏移了100px呢?这会涉及到二个问题,一个是参考点问题,一个是方向问题,首先说下参考点问题,相信知道对MotionEvent类都很熟悉,它封装了我们手指在屏幕上移动上一系列的操作,有按下,滑动,抬起等,它提供了getX(),getRawx()方法,那这个方法有什么区别呢?画图:



现在写例子来证明上面结论是否正确,

<RelativeLayout 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"

    >

    <LinearLayout

        android:id="@+id/ll_root"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:background="#ff0000"

        android:orientation="horizontal"

        android:gravity="center"

        android:layout_marginLeft="10px"

         >

         <TextView 

             android:id="@+id/tv1"

             android:layout_width="0dp"

             android:layout_height="60dp"

             android:text="item11111"

              android:layout_weight="1"

              android:gravity="center"

              android:background="#ff00ff"

             />

          <TextView 

             android:layout_width="0dp"

             android:layout_height="60dp"

             android:text="item22222"

             android:layout_weight="1"

             android:gravity="center"

             android:background="#00ffff"

             />

         </LinearLayout>

</RelativeLayout>

现在我LinearLayout向左是10px,

tv1.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float rawX = event.getRawX();
Log.e(TAG,"x="+x+":::rawX="+rawX);
break;
}
return false;
}
});

打印的log日记:



这二者是不是相差就是10个像素了,因为LinearLayout向左是10个px,就是离屏幕为10个px,我们也知道activity中也有个ouTouchEnevt()方法,这个event获取getX()和getRawX()都是以屏幕原点为参考点的,所以getX(),getRawX()方法获取的值是一样的,

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float rawX = event.getRawX();
Log.e(TAG,"x="+x+":::rawX="+rawX);
break;
}
return super.onTouchEvent(event);
}

这里打印的log就不贴了,我已经验证了,而scrollTo()它的偏移量是以什么为参考点呢?是以当前调用scrollTo()方法的view的左上角为原点(也就是参考点),这个其实很好验证,你把调用scrollTo()方法的view离屏幕左侧多少个px,发现这个view调用scrollTo()传递的x,y方向偏移量都是一样的,这就证明了它不是以屏幕的左上角为参考点,而是以view左上角为参考点,

现在考虑上面留下的第二个问题就是偏移量问题,为什么ll_root.scrollTo(30, 0);发现它内容是向左移动了30px,我们知道x向右是正方向也就是正值,y轴向下是正方向,



这就要看看scrollTo()源码了,也许从这能找到我们想要的答案,

 /**

     * Set the scrolled position of your view. This will cause a call to

     * {@link #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();

            }

        }

    }

从google的注释中意思是设置这个滑动view到某一个点,而不是距离,因为距离你可以累加,比如为什么你多次调用一个view的scrollTo(x,y)如果x,y值不变,发现它没动,现在结合源码解释下,

看到上面的代码有个if条件判断,x,y是我们从外面传递进来的值,mScrollX != x || mScrollY != y  这二个条件是关键,那么mScrollX和mScrollY表示啥意思呢?

 /**

     * The offset, in pixels, by which the content of this view is scrolled

     * horizontally.

     * {@hide}

     */

    @ViewDebug.ExportedProperty(category = "scrolling")

    protected int mScrollX;

    /**

     * The offset, in pixels, by which the content of this view is scrolled

     * vertically.

     * {@hide}

     */

    @ViewDebug.ExportedProperty(category = "scrolling")

    protected int mScrollY;

这是View源码中所定义的2个变量,它的注释意思是这个view滑动内容的偏移的像素,这有二个关键的词,一个是滑动内容,这个也就解释了博客中刚开始LinearLayout什么都没有的时候,调用scrollTo()方法没效果的原因,第二是偏移的像素点,View的源码中给我们提供了一个方法获取这个mScrollX值的方法就是getScrollX,现在我在没有调用view的scrollTo()方法之前调用getScrollX()和view调用scrollTo()后再调用getScrollX()看看这前后获取mScrollX这个值有什么不同,

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ll_root = (LinearLayout) findViewById(R.id.ll_root);
tv1 = (TextView) findViewById(R.id.tv1);
Log.e(TAG,"没有调用scrollTo方法前 mScroll的值为"+ll_root.getScrollX());
ll_root.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
ll_root.scrollTo(-30, 0);
Log.e(TAG,"调用scrollTo方法后 mScroll的值为"+ll_root.getScrollX());
}
});
}

log:



效果图:



发现它死向右滑动了30个像素,关于它为什么是向右滑动等下会解释,发现调用scrollTo(-30,0)之后它的mScrollX的值为-30,我现在对里面的变量简单做了下注释

if (mScrollX != x || mScrollY != y) {

            int oldX = mScrollX;//oldX就是记录上次x轴偏移量

            int oldY = mScrollY;//oldY就是记录上次y轴偏移量

            mScrollX = x;//记录当前x轴偏移量

            mScrollY = y;//记录当前y轴偏移量

            invalidateParentCaches();

            onScrollChanged(mScrollX, mScrollY, oldX, oldY);

            if (!awakenScrollBars()) {

                postInvalidateOnAnimation();//更新界面 要重新调用draw()方法

            }

        }

现在知道为什么多次调用同一个scrollTo(x,y)中x,y值没变而没效果的原因,因为你上次的偏移量和当前的偏移量(就是传递进去的x,y)相等,所以它没有调用if里面的postInvalidateOnAnimation()而没去重新绘制界面,我们知道重新绘制界面要调用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;

        if (!dirtyOpaque) {

            final Drawable background = mBackground;

            if (background != null) {

                final int scrollX = mScrollX;

                final int scrollY = mScrollY;

                if (mBackgroundSizeChanged) {

                    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);

                    mBackgroundSizeChanged = false;

                }

                if ((scrollX | scrollY) == 0) {

                    background.draw(canvas);

                } else {

                    canvas.translate(scrollX, scrollY);

                    background.draw(canvas);

                    canvas.translate(-scrollX, -scrollY);

                }

            }

        }

......................



draw()方法代码实在太多,在这就不方便贴,就贴了刚开始一部分,

 if (!verticalEdges && !horizontalEdges) {

            // Step 3, draw the content

            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children

            dispatchDraw(canvas);

            // Step 6, draw decorations (scrollbars)

            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {

                mOverlay.getOverlayView().dispatchDraw(canvas);

            }

            // we're done...

            return;

        }

这里有一个onDrawScrollBars(canvas);方法,在这个方法最后几行代码有一个重新绘制界面的方法

if (invalidate) {

        invalidate(left, top, right, bottom);

 }

然后跟进去看这个invalidate()方法

 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);

            }

        }

    }

看最后一个if条件也就是这几行代码

  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,我们知道如何一个view在屏幕上都是一个矩形所绘制上去的,

 tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);

这个l其实就是0,scrollX就是-30,这样矩形的 t - scrollY这个是没变的,还是0,现在画图分析:



这就是为什么调用了scrollTo(-30,0)是向左移动了,其实还有个概念问题,比如我LinearLayout调用scrollTo(30,0)而不是-30呢?效果会咋样



你会发现textview1有一部分看不见了,我们知道我们屏幕上所能看见的都是通过draw到Canvas上的,而Canvas是没有宽和高限制的,可以看作是内蒙大草原一样,无边无际,而我们所能看到的内容是有区域的,超过屏幕的宽和高是看不见的,

view还有一个scrollBy()方法,源码如下:

    /**

     * Move the scrolled position of your view. This will cause a call to

     * {@link #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);

    }

发现scrollBy()方法就是直接调用了scrollTo()方法,现在写个例子使用了scrollBy()方法

直接在activity的onTouchEvent()方法调用

@Override
public boolean onTouchEvent(MotionEvent event) {
ll_root.scrollBy(10, 0);
return super.onTouchEvent(event);
}

效果图:



scrollBy()是在之前的基础上累加x,y

现在另用scrollTo()实现下QQ滑动效果,

布局文件:

<RelativeLayout 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"

    >

    <LinearLayout

        android:id="@+id/ll_root"

        android:layout_width="match_parent"

        android:layout_height="50dp"

        android:background="#ffffff"

        android:orientation="horizontal"

         >

         <TextView 

             android:id="@+id/tv1"

             android:layout_width="match_parent"

             android:layout_height="50dp"

             android:text="你有未读的消息"

              android:gravity="center"

              android:background="#ffffff"

             />

          <TextView 

             android:layout_width="90px"

             android:layout_height="50dp"

             android:text="置顶"

             android:gravity="center"

             android:background="#e5e5e5"

             />

          <TextView 

             android:layout_width="90px"

             android:layout_height="50dp"

             android:text="标为已读"

             android:gravity="center"

             android:background="#ff00ff"

             />

          <TextView 

             android:layout_width="90px"

             android:layout_height="50dp"

             android:gravity="center"

             android:background="#ff0000"

             android:text="删除"

             />

         </LinearLayout>

</RelativeLayout>

MainActivity中的逻辑

public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private LinearLayout ll_root;
private TextView tv1;
private float downX = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ll_root = (LinearLayout) findViewById(R.id.ll_root);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
int scrollX = ll_root.getScrollX();
int newScrollX = (int) (scrollX+downX-moveX);
ll_root.scrollTo(newScrollX, 0);
downX = moveX;
break;
case MotionEvent.ACTION_UP:

break;
}
return super.onTouchEvent(event);
}

}

效果图:



发现是在activity的onTouchEnevt()方法中写滑动的逻辑,先分析下在touch中写的代码,为了分析,我在这个LinearLayout下面画点来记录我手指移动的轨迹,以及打log,

自定义的view

public class CustomPointView extends View {
private static final String TAG = "CustomPointView";
private Paint mPaint;
private float mPointX,mPointY;
private float prePointx,proPoingy;
private float[] pts;
private List<Float> lists;
public CustomPointView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setStrokeWidth(3);
lists = new ArrayList<Float>();
pts = new float[lists.size()];
}

public CustomPointView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setStrokeWidth(3);
lists = new ArrayList<Float>();
pts = new float[lists.size()];
}

public CustomPointView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setStrokeWidth(3);
lists = new ArrayList<Float>();
pts = new float[lists.size()];
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.RED);
if(lists.size()>0){
for(int i=0;i<lists.size();i++){
canvas.drawPoint(lists.get(i),30, mPaint);
}
}
}
public void setPoints(float x,float y){
this.mPointX = x;
this.mPointY = y;
lists.add(x);
invalidate();
}

}

在down的时候调用一下,在move时候调用一下,动态效果图:



下面的红线就是我画的手指在屏幕上移动的轨迹,分析如图:



好了,写到这里,洗澡睡觉!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: