Android 使用ViewGroup實現ViewPager的效果

ViewPager控件可以讓我們做出很多漂亮的界面,例如導航, 頁面菜單等. 那麼我們自己能否去實現ViewPager的效果呢? 本文將介紹如何使用ViewGroup + scrollTo + scroller實現ViewPager控件, 並且會簡單地實現一個自己的scroller, 來了解學習系統提供的scroller類滑屏功能的實現思想.
首先看一下實現的效果:

  1. 定義自己的ViewPager類–MyViewPager繼承ViewGroup
public class MyViewPager extends ViewGroup {
    private Context context;
    //手勢識別工具類
    private GestureDetector detector;

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        initView();
    }

    private void initView() {

        detector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {

            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return false;
            }

            //處理移動事件
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                //將當前視圖內容偏移(x , y)個單位,可視區域也跟着偏移(x,y)個單位
                //也就是說讓視圖跟着鼠標移動, distanceX爲鼠標在屏幕上移動的距離
                scrollBy((int) distanceX, 0);
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {

            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                return false;
            }
        });
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //設置每個子view的位置
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        detector.onTouchEvent(event);

        return true;
    }

}

(1) 在MyViewPager中, 重寫OnLayout方法, 在OnLayout方法中去確定子view在ViewPager中的位置:

通過getChildAt獲得所有的子view, 調用子view的layout方法設置每個子view的位置, layout接收4個參數(就是左上角座標與右下角座標), 來確定view的大小, 如上圖可以分析出每個view在MyViewPager中的位置, 例如第2個view的位置是:
[getWidth(), 0, 2 * getWidth(), getHeight()] , getWidth 與 getHeight爲MyViewPager的寬高. 我們可以看到變化的只有橫座標.

(2) 重寫onTouchEvent方法, 此方法處理屏幕觸摸事件, 在這裏使用用戶手勢識別工具類GestureDetector來處理action.move事件, 在OnGestureListener的onScroll中處理move事件, onScroll方法的參數distanceX, 就是工具類幫我們計算好的手指在屏幕上x軸方向移動的距離, 然後就可以很方便的使用此參數, 調用scrollBy(x, y)方法移動視圖, 讓視圖偏移(x, y). 這樣就實現了MyViewPager隨手指移動而移動.

2.使用MyViewPager. 並給MyViewPager設置子view

public class MainActivity extends AppCompatActivity {

    private MyViewPager myViewPager;
    private int[] imgIds = new int[] {
            R.mipmap.a, R.mipmap.b, R.mipmap.c,
            R.mipmap.d, R.mipmap.e, R.mipmap.f
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myViewPager = (MyViewPager) findViewById(R.id.my_viewpager);

        for (int i = 0; i < imgIds.length; i++) {
            ImageView imageView = new ImageView(this);
            imageView.setImageResource(imgIds[i]);
            myViewPager.addView(imageView);
        }
    }
}

佈局文件:

<?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="com.myviewpager.MainActivity">

    <com.myviewpager.MyViewPager
        android:id="@+id/my_viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

給MyViewPager設置6張圖片. 現在效果圖如下:

3.現在圖片可以跟隨手指滑動而滑動, 但是還不能自動地切換, 圖片只能停在你移動到的地方, 如果想實現切換的效果, 還需要我們自己去處理touch事件, 在onTouchEvent中繼續添加代碼:

//標記當前顯示在屏幕上的圖片
private int currPos = 0;
//記錄按下時的橫座標
private int firstX = 0;

@Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        detector.onTouchEvent(event);
        //添加下面的代碼
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstX = getScrollX();    //獲得按下去時的橫座標
                break;
            case MotionEvent.ACTION_UP:   //判斷顯示哪個子view, 如果滑動大於父控件的一半切換子view
                int tmpPos = currPos;
                if ((getScrollX() - firstX) > getWidth() / 2) {    //向左滑動
                    tmpPos++;
                } else if ((firstX - getScrollX()) > getWidth() / 2) {  //向右滑動
                    tmpPos--;
                }
                MoveToDest(tmpPos);  //切換到指定的圖片
                break;
            default:
                break;
        }
        return true;
    }

    public void MoveToDest(int tmpPos) {
        //確定currPos的值, 保證currPos的範圍在[0, getChildCount() - 1]
        currPos = tmpPos > 0 ? tmpPos : 0;
        currPos = currPos < getChildCount() - 1 ? currPos : (getChildCount() - 1);
        //將視圖內容偏移至(x , y)座標處,可視區域位於(x , y)座標處
        scrollTo(currPos * getWidth(), 0);
    }

定義兩個成員變量, firstX, currPos來記錄點擊屏幕時的點與當前顯示在屏幕上的子view. 在touch_up事件中處理: 當在屏幕上滑動的距離大於屏幕的一半切換視圖, 否則留在當前視圖. 在MoveToDest方法中調用scrollTo來實現.
現在的效果爲:

4.在ViewPager中, 我們去切換視圖時, 並不是瞬間完成, 而是有個過程.
我們實現MyScroller類來實現滑動過程,新建MyScroller類:

public class MyScroller {

    private Context context;
    private int disX;
    private int startY;
    private int startX;
    private int disY;
    private long startTime; //開始動畫時間
    private boolean isFinish; //標誌是否結束動畫
    //默認運行時間,500ms
    private int duration = 500;
    //當前繪製所在的X位置
    private long currX;
    //當前繪製所在的Y位置
    private long currY;

    public MyScroller(Context context) {
        this.context = context;
    }

    public long getCurrY() {
        return currY;
    }

    public void setCurrY(long currY) {
        this.currY = currY;
    }

    public long getCurrX() {
        return currX;
    }

    public void setCurrX(long currX) {
        this.currX = currX;
    }



    /**
     * 開始移動
     * @param startX  開始時的x座標
     * @param startY  開始時的y座標
     * @param disX    x方向要移動的距離
     * @param disY    y方向要移動的距離
     */
    public void startScroll(int startX, int startY, int disX, int disY) {
        this.startX = startX;
        this.startY = startY;
        this.disX = disX;
        this.disY = disY;
        this.startTime = SystemClock.uptimeMillis();
        this.isFinish = false;
    }

    /**
     * 計算當前的運行狀況
     * @return
     * true 還在運行
     * false 運行結束
     */
    public boolean computeScrollOffset() {
        if (isFinish) {
            return false;
        }
        //獲得繪製時的時間
        long passTime = SystemClock.uptimeMillis() - startTime;
        //如果時間還在允許的範圍內
        if (passTime <= duration) {
            currX = startX + disX * passTime / duration;
            currY = startY + disY * passTime / duration;
        } else { //繪製運行結束
            currX = startX + disX;
            currY = startY + disY;
            isFinish = true;
        }
        return true;
    }

}

在startScroll方法中記錄下當前滑動點座標與目標點座標, 並記錄下當前時間. 然後在computeScrollOffset中開始去更新當前的滑動的座標.

接下來使用MyScroller來實現滑動過程,在MyViewPager類中定義MyScroller類成員變量並去使用它來實現滑動過程:

private MyScroller myScroller;

//在initView中初始化
private void initView() {
   ...............
   myScroller = new MyScroller(context);
   ...............

然後修改moveToDest方法,不去使用ScrollTo來實現移動,使用MyScroller來實現:

    public void moveToDest(int nextId) {
    ..................
    //不使用scrollTo來實現移動
//      scrollTo(currId * getWidth(), 0);
        //獲得移動的距離,移動距離等於最終位置-當前位置
        int distance = currId * getWidth() - getScrollX();
        myScroller.startScroll(getScrollX(), 0, distance, 0);
        //會導致computeScroll方法執行
        invalidate();
    }

    @Override
    public void computeScroll() {
        //計算當前繪製狀況,進行繪製
        if (myScroller.computeScrollOffset()) {
            int nowX = (int) myScroller.getCurrX();
            scrollTo(nowX, 0);
            invalidate();
        }
    }

現在的效果你會看到視圖切換時就會有個過程而不是很快就完成了:

現在視圖的切換是勻速的,如果想達到ViewPager那種加速效果,將MyScroller改成系統提供的Scroller類即可,只需要將private MyScroller myScroller; 改爲private Scroller myScroller; 其他都不需要動就可以實現加速效果, 因爲我們的MyScroller類的接口和系統Scroller接口一樣。

5.現在MyViewPager中的View全爲ImageView, 那麼向MyViewPager中添加一個佈局(包括一個button和listView)來看一下是否同樣的支持滑動效果.

(1)佈局文件list_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/list_layout"
    android:orientation="vertical" >

    <Button 
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_margin="10dp"
        android:text="button"/>
    <ListView 
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"/>

</LinearLayout>

(2)我們將佈局文件加載到MyViewPager的第四個view

    private LinearLayout listLayout;
    private ListView listview;
    private String[] datas = { "Apple", "Banana", "Orange", "Watermelon",
            "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango" };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
 ............
 //添加下面代碼
        listLayout = (LinearLayout) LayoutInflater.from(getApplicationContext())
        .inflate(R.layout.list_view, null);
        listview = (ListView) listLayout.findViewById(R.id.listview);

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                MainActivity.this, android.R.layout.simple_list_item_1, datas);
        listview.setAdapter(adapter);

        for (int i = 0; i < ids.length; i++) {
            if (i == 3) {  //將佈局文件添加到第四個位置
                msv.addView(listLayout);
            } else {
                ImageView imageView = new ImageView(this);
                imageView.setBackgroundResource(imgIds[i]);
                msv.addView(imageView);
            }
        }

    }

這個時候你運行會發現這個佈局不會顯示出來, 因爲這個時候我們沒有在MyViewPager中去調用佈局控件的OnMeasure方法, 讓其去測量子View的大小, 導致佈局view沒有顯示出來,所以我們需要在MyViewPager中去重載OnMeasure方法,去調用每個子view的measure,繪製子view的大小。

    //繪製每個子控件的尺寸
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.measure(widthMeasureSpec, heightMeasureSpec);
        }
    }

現在的效果爲:

現在我們發現,當滑動到佈局文件中,在listview上可以上下滑,但是此時左右滑動失效了,不能左右滑動。

原因因爲Touch事件的傳遞導致, 我們知道touch事件是從父view到子view一層一層傳遞,當我們左右滑動時,此事件由MyViewPager一層層傳遞到listview,但是listview並不支持(消費)此事件,所以導致左右滑動不起作用了, 現在我們重寫onInterceptTouchEvent,來去判斷touch事件,如果是左右滑動事件那麼就去中斷此事件,不讓其再往下傳遞,我們去處理消費它。

    /**
     * 返回true, 中斷事件,執行自己的onTouchEvent方法
     * 返回false, 默認處理,不中斷事件, 也不會執行自己的onTouchEvent方法
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = false;
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //解決點擊圖片時跳動的bug
            detector.onTouchEvent(ev);

            firstX = (int) ev.getX();
            firstY = (int) ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            int disx = (int) Math.abs(ev.getX() - firstX); //不管是左右移,只判斷是否左右移動
            int disy = (int) Math.abs(ev.getY() - firstY); //豎直方向移動距離
            //判斷是否爲水平方向移動,disx > 10 防止手指按住屏幕抖動
            if(disx > disy && disx > 10) {
                result = true;
            } else {
                result = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
        }
        return result;
    }

現在效果如下:

此時還缺少一點就是開放一個接口讓外部來使用, 比如導航界面,點擊某一個點,會直接跳到那個點指定的頁面。
在MyViewPager中添加接口:

    //監聽器對象
    private MyPagerChangedListener listener;

    public MyPagerChangedListener getListener() {
        return listener;
    }

    public void setListener(MyPagerChangedListener listener) {
        this.listener = listener;
    }
    //頁面改變時的監聽接口
    public interface MyPagerChangedListener{
        void moveToDest(int currId);
    }

然後在moveToDest方法中去判斷此監聽器是否爲空,不爲空就調用其接口。

//移動到指定的子控件上
    public void moveToDest(int nextId) {
        ..................

        //觸發listener事件
        if (listener != null) {
            listener.moveToDest(currId);
        }
        ..............
    }

至此, 使用ViewGroup實現ViewPager效果完成, 如有問題可以留言。
源碼下載地址:

http://download.csdn.net/detail/lbcab/9536961

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