初探Android Scroll——scrollTo()与scrollBy()

Scroll是android中很常见的交互方式,使用得好可以使我们的应用更具有体验性。本着学习与探究的精神,我尝试来解析一下我所知道的android scroll,这篇文章是我对scrollTo()和scrollBy()的一些理解。

1. View边界问题

在学习ScrollTo()和ScrollBy()之前,我们有必要来了解一下android的座标系,如下图所示:
android座标系
通常我们所看到的就是这个座标系,也就是手机的左上角为原点(0,0)水平方向为x轴,垂直方向为y轴,整个屏幕就是个在x和y轴正方向的有限矩形区域。我们再来看下android的ui结构,如下图:
android ui 结构
可以看到,我们所能看到的手机屏幕视图就是一个ViewGroup,而ViewGroup也是继承自View。回顾一下view的绘制,可以知道,其实我们所看到的视图内容是画在画布Canvas上的,当然canvas也拥有一个座标系,我们在通过drawXXX()方法画图时就需要知道图形的具体座标。
view与canvas的座标系
因为我们一般都是以我们屏幕(或者说view)为参考系的,view本身的变化,我们肉眼是看不到的,只能看到相对view的canvas的变化。所以我们通过移动canvas可以造成view内容移动的视觉。
我们来定义一个view,重写onDraw()画出一个200*200的蓝色矩形,以下是java代码:

publicclass MyViewextends View {


    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(0,0,200,200,paint);
    }
}

呈现出来的如下(由于布局代码很简单,就不贴上来了):
自定义view1
从代码中我们可以看到这样一个过程:我们拿起笔Paint,在画布Canvas上画了一个我们想要的图形,蓝色的正方形,然后通过View看到了它。就好像一个画家画了一幅画,然后把它镶嵌到画框里,我们通过画框,看到了画里有一个人。但如果画家本来是画了一群人,只是把显示一个人的画的部分放到画框里让我们看到呢?再来看,View就是一个画框,Canvas就是画布,Paint是笔,那么我们所看到是不是视图的部分呢?接下来我们来证实这个想法。DrawRect()前4个参数是X,Y轴的起始和结束位置,我们尝试把它设为负数看看有什么效果。

canvas.drawRect(-100,0,200,200,paint);

自定义view2
显示的视图并没有什么不一样,显然“画框”View的左上角是和(0,0)重合了。Canvas中有一个translate(x,y),它起到平移画布的效果,我们将画布右移100,然后再将之前的矩形画到上面。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint = new Paint();
    paint.setColor(Color.BLUE);
    paint.setStyle(Paint.Style.FILL);
    canvas.save();
    canvas.translate(100,0);
    canvas.drawRect(-100,0,200,200,paint);
    canvas.restore();
}

这时视图变成以下这样了:
自定义view3
显然我们画在x轴负轴的部分也被显示出来了,这就说明,画布的内容并不只是我们看到的这些,可能有很多内容是被View这个块中空的板遮住了。X,Y的二维平面式没有边界的,所以我们在画布上画的画理论上是无限大的,View只是截取了一部分让我们看到。
我看了一些有关view绘制的文章,有些作者喜欢把View说成是无界的,我觉得这种说法不准确,应该说view是有界的,而Canvas是无界的。我更喜欢View是一个中空的不透明板的说法,我们透过view无限绘图世界的一小部分。

2. 终于到ScrollTo()和ScrollBy()了

或许上面讲了一堆废话,但是尝试了解一些原理,并自己把自己的理解写下来并不是一件容易的事。 ScrollTo()和ScrollBy()是View类通过api,可以通过他们滑动view(其实是滑动view的内容)。通过字词就可以理解,ScrollTo()是滑动到目标位置,ScrollBy()是滑动所给偏移量。我们看下ScrollBy()的实现:

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

其实对ScrollTo()再次包装,而mScrollX和mScrollY是view相对canvas的偏移量,可以通过view实例方法getScrollX()和getScollY()获取他们的值。我们来测试一下ScrollTo(),布局就只添加一个TextView,部分java代码如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test);
    TextView tv = (TextView) findViewById(R.id.tv);
    Log.i("TestActivity",tv.getScrollX()+","+tv.getScrollY());
    tv.scrollTo(100, 0);
    Log.i("TestActivity", tv.getScrollX() + "," + tv.getScrollY());
}

先输出tv的初始ScrollX和ScrollY,然后滑动到(100,0)再输出当前的结果如下:
输出log
我们自定义一个view,来看使用scrollTo()的效果。MyView的java代码:

public class MyView extends View {


    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.YELLOW);
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(-100,0,200,200,paint);
    }
}

布局中加入MyView控件:

<com.forzero.moveview.MyView
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:id="@+id/view2" />

Activity的java代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test);
    MyView view2 = (MyView) findViewById(R.id.view2);
    view2.scrollTo(-100, 0);
}

运行前的MyView视图如下:
运行前
运行后如下:
运行后
通过scrollTo(),可以把之前隐藏的蓝色部分显示出来了,这就证明scrollTo()和scrollBy()只是移动View的内容,我们想要移动view本身可以对其父容器操作,代码如下:

((View)myView.getParent()).scrollBy(-100, 0);

值得注意的是,由于偏移量是view相对canvas的,所以要达到想要的效果则需取数值的相反数。

3. 一个Demo

布局:

<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context=".MainActivity">


    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true">

        <ImageView
            android:id="@+id/view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:src="@mipmap/ic_launcher" />
    </RelativeLayout>


    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="x:100" />

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/btn1"
        android:text="x:-100" />

    <Button
        android:id="@+id/btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/btn2"
        android:text="y:100" />

    <Button
        android:id="@+id/btn4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/btn3"
        android:text="y:-100" />
</RelativeLayout>

java代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private ImageView myView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myView  = (ImageView) findViewById(R.id.view);

        findViewById(R.id.btn1).setOnClickListener(this);
        findViewById(R.id.btn2).setOnClickListener(this);
        findViewById(R.id.btn3).setOnClickListener(this);
        findViewById(R.id.btn4).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn1:
                ((View)myView.getParent()).scrollBy(-100, 0);
                break;
            case R.id.btn2:
                ((View)myView.getParent()).scrollBy(100, 0);
                break;
            case R.id.btn3:
                ((View)myView.getParent()).scrollBy(0, -100);
                break;
            case R.id.btn4:
                ((View)myView.getParent()).scrollBy(0, 100);
                break;
        }
    }
}

效果截图:
效果截图

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章