初探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;
        }
    }
}

效果截圖:
效果截圖

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