Scroll是android中很常见的交互方式,使用得好可以使我们的应用更具有体验性。本着学习与探究的精神,我尝试来解析一下我所知道的android scroll,这篇文章是我对scrollTo()和scrollBy()的一些理解。
在学习ScrollTo()和ScrollBy()之前,我们有必要来了解一下android的坐标系,如下图所示:
通常我们所看到的就是这个坐标系,也就是手机的左上角为原点(0,0)水平方向为x轴,垂直方向为y轴,整个屏幕就是个在x和y轴正方向的有限矩形区域。我们再来看下android的ui结构,如下图:
可以看到,我们所能看到的手机屏幕视图就是一个ViewGroup,而ViewGroup也是继承自View。回顾一下view的绘制,可以知道,其实我们所看到的视图内容是画在画布Canvas上的,当然canvas也拥有一个坐标系,我们在通过drawXXX()方法画图时就需要知道图形的具体坐标。
因为我们一般都是以我们屏幕(或者说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);
}
}
呈现出来的如下(由于布局代码很简单,就不贴上来了):
从代码中我们可以看到这样一个过程:我们拿起笔Paint,在画布Canvas上画了一个我们想要的图形,蓝色的正方形,然后通过View看到了它。就好像一个画家画了一幅画,然后把它镶嵌到画框里,我们通过画框,看到了画里有一个人。但如果画家本来是画了一群人,只是把显示一个人的画的部分放到画框里让我们看到呢?再来看,View就是一个画框,Canvas就是画布,Paint是笔,那么我们所看到是不是视图的部分呢?接下来我们来证实这个想法。DrawRect()前4个参数是X,Y轴的起始和结束位置,我们尝试把它设为负数看看有什么效果。
canvas.drawRect(-100,0,200,200,paint);
显示的视图并没有什么不一样,显然“画框”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();
}
这时视图变成以下这样了:
显然我们画在x轴负轴的部分也被显示出来了,这就说明,画布的内容并不只是我们看到的这些,可能有很多内容是被View这个块中空的板遮住了。X,Y的二维平面式没有边界的,所以我们在画布上画的画理论上是无限大的,View只是截取了一部分让我们看到。
我看了一些有关view绘制的文章,有些作者喜欢把View说成是无界的,我觉得这种说法不准确,应该说view是有界的,而Canvas是无界的。我更喜欢View是一个中空的不透明板的说法,我们透过view无限绘图世界的一小部分。
或许上面讲了一堆废话,但是尝试了解一些原理,并自己把自己的理解写下来并不是一件容易的事。 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)再输出当前的结果如下:
我们自定义一个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的,所以要达到想要的效果则需取数值的相反数。
布局:
<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;
}
}
}