自定義View 篇三 《手動打造ViewPage》

有了之前自定義View的理論基礎,有了ViewPage、事件分發機制、滑動衝突、Scroller使用等相關知識的鋪墊,今天純手動打造一款ViewPage。

1、完成基本的顯示:

在MainActivity中:

public class MainActivity extends AppCompatActivity {

    private MyViewPage mViewPage;

    int[] imageIds = new int[]{
           R.drawable.pic_0,
           R.drawable.pic_1,
           R.drawable.pic_2,
           R.drawable.pic_3,
           R.drawable.pic_4,
           R.drawable.pic_5
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mViewPage = (MyViewPage) findViewById(R.id.myviewpage);

        //給自定義ViewPage添加孩子組件
        for (int i = 0; i < imageIds.length; i++) {
            ImageView imageView = new ImageView(this);
            imageView.setBackgroundResource(imageIds[i]);
            mViewPage.addView(imageView);
        }

    }
}
在MyViewPage中:

public class MyViewPage extends ViewGroup {

    public MyViewPage(Context context) {
        this(context,null);
    }

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            //遍歷所有孩子,手動安放每個孩子控件的位置
           getChildAt(i).layout(i*getWidth(),0,(i+1)*getWidth(),getHeight());
        }
    }
}

首先往自定義view裏面添加了6張圖片,在view的onLayout方法中,給每個孩子組件進行佈局安放位置,因爲位置都確定了,因而不用去進行測量和繪製也可以顯示。

給每個孩子佈局位置的算法如下:


此時運行:


2、實現可滑動效果

運行後,按照添加的順序顯示,第一張肯定顯示的是第一個孩子控件對象的圖片。但是此時是無法進行滑動的,我們使用手勢識別器GestureDetector,讓自定義的控件可以滑動:

private void init() {
    mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
        //手勢識別器移動的監聽回調。每次移動,都會回調該方法
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            //參數1:起點動作封裝;參數2:終點動作封裝;參數3x方向的移動距離;參數4y方向滑動距離
            scrollBy((int) distanceX,0);
            return true;
        }
    });
}
初始化手勢識別器,重寫手勢滑動監聽回調。每當滑動的時候,都會調用這裏的方法,我們在這裏直接調用ScrollBy()方法,更新當前控件(MyViewPage)內部孩子控件(圖片)的位置。在這裏需要知道ScrollBy()ScrollTo()的區別:


scrollBy來移動一段相對的距離,屬於溫柔性質的。表示在原來座標的基礎上再改變參數大小的距離。對於x:大於零表示往左移,小於零表示往右移。對於y:大於零表示往上移,小於零表示往下移。
scrollBy(10, 0);從右往左移動10個像素
scrollBy(-10, 0);從左往右動10個像素

scrollTo就是把View移動屏幕的X和Y位置,屬於強迫性質的。參數代表我一次性跳躍到該座標位置。
我擦,中國語言真是博大精深啊~

   瞬間移動視圖的內容: 利用View的scroll方法
    1). scrollBy(int x, int y) : 滑動指定的偏移量(從當前位置瞬間)
     x: x軸上的偏移量, x>0內容向左滑動, x<0內容向右滑動, x=0水平方向不滑動
     y: y軸上的偏移量, y>0內容向上滑動, y<0內容向下滑動, y=0垂直方向不滑動
    2). scrollTo(int x, int y) : 滑動到指定的偏移量(從當前位置瞬間)
     x: 目標位置x軸上的偏移量, x>0移動到原始位置的左側, x<0移動到原始位置的右側,x=0移動到水平原始位置,
     y: 目標位置y軸上的偏移量, y>0移動到原始位置的上側, y<0移動到原始位置的下側, y=0移動到垂直原始位置
 
View類的源代碼如下所示,mScrollX記錄的是當前View針對屏幕座標在水平方向上的偏移量(getScrollX();),而mScrollY則是記錄的時當前View針對屏幕在豎值方向上的偏移量(getScrollY();)。

    scrollTo就是把View移動到屏幕的X和Y位置,也就是絕對位置。而scrollBy其實就是調用的scrollTo,但是參數是當前mScrollX和mScrollY加上X和Y的位置,所以ScrollBy調用的是相對於mScrollX和mScrollY的位置。

手勢識別器要想使用,需要把touch事件委託給手勢識別器來處理:

@Override
public boolean onTouchEvent(MotionEvent event) {
    //事件委託交手勢識別器
    mGestureDetector.onTouchEvent(event);
    return true;
}
運行程序效果:


可以看到,此時我們實現了滑動效果。

3、滑動到某個位置後自動到合適位置下標停留

此時的滑動顯然是不可行的,我們需要跟ViewPage那樣,滑動小於屏幕一半時,跳轉到當前頁面,滑動距離大於一半時,跳轉到下一頁。讓我們來實現利邏輯吧:

switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        break;
    case MotionEvent.ACTION_MOVE:
        break;
    case MotionEvent.ACTION_UP:
        //鬆開手的時候,根據當前位置,來確定下一個頁面
        int scrollX = getScrollX();
        //當前頁的索引值
        int pageIndex = scrollX / getWidth();
        int offset = scrollX % getWidth();
        if(offset > getWidth()/2){
            pageIndex++;
        }
        //處理越界問題
        if(pageIndex > getChildCount()-1){
            pageIndex = getChildCount()-1;
        }
         goCurrentPage(pageIndex);
        break;
    default:
        break;
    }

我們重寫了touch事件,因爲監聽到底是否在頁面一半位置,是手指離開屏幕時候決定的,因而邏輯寫在UP事件裏面即可。相信這個小算法難不倒你。

當前的pageIndex就是對應的頁碼,而且處理了越界問題。最後再生成一個方法,專門用於跳轉頁面功能。具體代碼如下:

/**
 * 根據當前
 * @param pageIndex
 *      當前的page頁面
 */
private void goCurrentPage(int pageIndex) {

    scrollTo(pageIndex*getWidth(),0);
}
是的,只需要一行代碼就可以了!運行起來看看效果吧:


此時跟原生的ViewPgae挺像了,但是還是稍有區別的,即滑動跳轉很是生硬。這是由於使用了ScrollTo()進行了強制跳轉的緣故。爲了與ViewPage更貼近,我們使用系統提供的類:Scoller來解決生硬問題。

Scoller的具體用法可以參考博客:Android Scroller完全解析

4、回彈過程解決辦法

修改ScrollTo(),被Scoller取代:

private void goCurrentPage(int pageIndex) {
    //scrollTo(pageIndex*getWidth(),0);

    int dx = pageIndex*getWidth() - getScrollX();

    Log.e("YDL",dx+"-------");

    //參數1x的起始值;參數2y的起始值;參數3x的偏移量;參數4y的偏移量
    //對於參數3dx>0往左移動;dx<0往右移動
    mScroller.startScroll(getScrollX(),0,dx,0,Math.abs(dx));//dx絕對值作爲時間值,按比例可以實現了勻速移動
    //使用Scroller必須重新刷新界面,不刷新的話不會滑動
    invalidate();
}
在這裏需要藉助Scroller來完成後續的滾動操作。接下來我們就調用startScroll()方法來初始化滾動數據並刷新界面。startScroll()方法接收四個參數,第一個參數是滾動開始時X的座標,第二個參數是滾動開始時Y的座標,第三個參數是橫向滾動的距離,正值表示向左滾動,第四個參數是縱向滾動的距離,正值表示向上滾動。緊接着調用invalidate()方法來刷新界面。

在這裏比較難理解的可能是dx值的計算方式。我也通過兩張圖片來分析裏面的算法:

圖一:移動距離超過半個屏幕,應該執行跳轉下一頁的功能。因此dx=i*getWidth()-getScrollX();可以自行測試。


圖二:移動的距離小於屏幕一半,執行跳轉上一頁的功能:


現在前兩步都已經完成了,最後我們還需要進行第三步操作,即重寫computeScroll()方法

//Scroller使用調用invalidate();後,會同步調用computeScroll()方法
@Override
public void computeScroll() {
    if(mScroller.computeScrollOffset()){
        int currX = mScroller.getCurrX();
        Log.e("YDL",currX+"");
        scrollTo(mScroller.getCurrX(),0);
        //也要刷新界面
        invalidate();
    }
}

並在其內部完成平滑滾動的邏輯 。在整個後續的平滑滾動過程中,computeScroll()方法是會一直被調用的,因此我們需要不斷調用Scroller的computeScrollOffset()方法來進行判斷滾動操作是否已經完成了,如果還沒完成的話,那就繼續調用scrollTo()方法,並把Scroller的currenX和currentY座標傳入,然後刷新界面從而完成平滑滾動的操作。
那麼我們運行程序看看效果吧:


可以看到,效果跟系統自帶的ViewPage幾乎一模一樣了。那麼最後,再分析一下Scroller進行滾動的原理吧。


平滑移動視圖的內容: 利用Scoller和View的scroll方法
    1). Scoller是實現View平滑移動的幫助類, 它本身並不能實現對View的移動
    2). 平滑移動的基本原理: 將整個從起始位置到結束位置的移動分解成多個小的距離, 循環調用scrollTo()實現平滑移動
    3). 相關API:
     a. Scoller類:
       -->Scoller(Context context) : 創建對象的構造方法
       -->startScroll(int startX, int startY, int dx, int dy, int duration) : 開始平滑移動視圖(這個方法本身不會產生滑動)
         startX : 起始位置的X偏移量
         startY : 起始位置的Y偏移量
         dx: 滑動多大的X偏移量(如果是0,X方向不會滑動)
         dy: 滑動多大的Y偏移量(如果是0,Y方向不會滑動)
         duration : 整個過程持續的時間(ms)
       -->startScroll(int startX, int startY, int dx, int dy): 開始平滑移動視圖(時間爲250ms)
       -->boolean computeScrollOffset() : 計算當前移動的偏移量, 並將其保存到Scoller對象中, 如果滑動還沒有完成返回true
       -->int getCurrX() : 得到計算出的X偏移量
       -->int getCurrY() : 得到計算出的Y偏移量
     b. View類
       -->invalidate() : 強制重繪, 導致draw()-->computeScroll()
         在scoller.startScroll()後必須執行此方法
       -->computeScroll() : 需要重寫此方法, 用於計算移動, 此方法在draw()中調用
         調用scoller計算移動偏移量
         調用view對象scrollTo()到計算出的偏移量
         調用View對象invalidate()強制重繪, 導致computeScroll()再次執行


我們在上面的代碼中可以看到當我們手指不段移動屏幕時,就會調用scrollBy來移動一段相對的距離。而當我們手指鬆開後,會調用mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);來產生一段動畫來移動到相應的頁面,在這個過程中系統會不斷調用computeScroll(),我們再使用scrollTo來把View移動到當前Scroller所在的絕對位置。


到目前爲止,該自定義ViewPage控件算是講完了。

源碼下載地址


打開微信掃描下方二維碼查看更多安卓文章:


打開微信搜索公衆號    Android程序員開發指南   或者手機掃描下方二維碼 在公衆號閱讀更多Android文章。

微信公衆號圖片:




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