Scroll是android中很常見的交互方式,使用得好可以使我們的應用更具有體驗性。本着學習與探究的精神,我嘗試來解析一下我所知道的android scroll,這篇文章是我對scrollTo()和scrollBy()的一些理解。
1. View邊界問題
在學習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無限繪圖世界的一小部分。
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)再輸出當前的結果如下:
我們自定義一個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;
}
}
}
效果截圖: