Android自定義ViewPager(一)——自定義Scroller模擬動畫過程

       轉載請註明出處:http://blog.csdn.net/allen315410/article/details/41575831

       相信Android SDK提供的ViewPager組件,大家實在是熟悉不過了,但是ViewPager存在於support.v4包下的,說明ViewPager並不存在於早期的android版本中,那麼如何在早期的android版本中也同樣使用類似於ViewPager一樣的滑動效果呢?這裏,我們還是繼續探討一下andrid的自定義組件好了,並且這篇博文只探討android的一些知識,並不是刻意去構建一個自定義的ViewPager去使用,這個是沒有必要的,請將注意力集中在實現這個效果的知識點上,方便以後“舉一反三”。


       好了,我們先來簡單分析一下ViewPager。ViewPager可以看做是一個“容器”,在這個“容器”裏可以擺放各種各樣的View類型,例如ViewPager每個分頁上可以放置TextView,ImageView,ListView、GridView等等一系列View組件,實際上這些View在ViewPager上的擺放我們可以看做是在ViewGroup上Layout各種View(實際上,這個實現是比較複雜的,這裏做個比喻意義而已),所以我們就可以抽象理解爲,ViewPager相當於ViewGroup,並且在這個ViewGroup上Layout各種View,所以接下來的代碼中,我們主要需要一個自定義的ViewGroup來實現達到這樣的效果。另外,還需要在這個ViewGroup上給每個分頁上的View添加一個左右滑動的效果,以求模擬出ViewPager上的動態效果。

       關於自定義ViewGroup的結構,我們有必要仔細探討一下,某些概念還是值得去加深理解的,爲了理解方便,請參看下面的“草圖”:


         從上面的草圖可以看到,紅色的邊框代表設備屏幕,即我們可以用肉眼看見的地方,整個灰色的大邊框代表整個效果,這裏稱爲“視圖”,每個視圖又分爲3個View,這個3個或者多個View組成一張很大的視圖。我們要弄清楚,這三者的關係,設備屏幕代表的顯示區域,即我們在設備上能看見的範圍,View代表的是單個的組件,一個屏幕上可以顯示一個或者多個View,但是視圖是最容易混淆的東西,視圖理論上是很大的一塊區域,它不但包括設備屏幕上能被肉眼看見的一部分,還包括設備屏幕以外肉眼看不見的地方,就如上圖所示的,子View2和子View3也是視圖的一部分,但是在設備屏幕之外,就是肉眼看不見的區域了。視圖裏可以存放很多的View,視圖被用來管理View的顯示效果。而且,視圖是可以自由活動的,通過控制視圖的活動,控制視圖在設備屏幕上的顯示範圍,就可以切換不同的分頁了。       


       所以接下來,我們主要去做的就是如何去自定義一個視圖,如何讓視圖展示不同的View在設備屏幕上,在Android上管理多個View的顯示可以通過自定義的ViewGroup,實現onLayout給View進行排版,初始化排版的時候,我一共向ViewGroup裏添加了6個子View,這6個子View呈水平橫向排版,如上圖所示的那樣,每個View顯示的寬度和高度跟父View(ViewGroup)相同,首次排版呈現出第一個子View在屏幕上,其他5個子View以次添加進來,以父View的寬度的N倍數排版,都被隱藏在設備屏幕的右邊區域。下面是自定義ViewGroup的實現代碼:

package com.example.myviewpager;

import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

public class MyViewPager extends ViewGroup {

	/** 手勢識別器 */
	private GestureDetector detector;
	/** 上下文 */
	private Context ctx;
	/** 第一次按下的X軸的座標 */
	private int firstDownX;
	/** 記錄當前View的id */
	private int currId = 0;
	/** 模擬動畫工具 */
	private MyScroller myScroller;

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

	private void init() {
		myScroller = new MyScroller(ctx);
		detector = new GestureDetector(ctx,
				new GestureDetector.OnGestureListener() {

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

					@Override
					public void onShowPress(MotionEvent e) {
					}

					@Override
					public boolean onScroll(MotionEvent e1, MotionEvent e2,
							float distanceX, float distanceY) {
						// 手指滑動
						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
					public boolean onDown(MotionEvent e) {
						return false;
					}
				});
	}

	/**
	 * 對子View進行佈局,確定子View的位置 changed 若爲true,
	 * 說明佈局發生了變化 l\t\r\b 指當前View位於父View的位置
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		for (int i = 0; i < getChildCount(); i++) {
			View view = getChildAt(i);
			// 指定子View的位置 ,左、上、右、下,是指在ViewGroup座標系中的位置
			view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(),
					getHeight());
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		detector.onTouchEvent(event); // 指定手勢識別器去處理滑動事件
		// 還是得自己處理一些邏輯
		switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN : // 按下
				firstDownX = (int) event.getX();
				break;
			case MotionEvent.ACTION_MOVE : // 移動
				break;
			case MotionEvent.ACTION_UP : // 擡起
				int nextId = 0; // 記錄下一個View的id
				if (event.getX() - firstDownX > getWidth() / 2) {
					// 手指離開點的X軸座標-firstDownX > 屏幕寬度的一半,左移
					nextId = (currId - 1) <= 0 ? 0 : currId - 1;
				} else if (firstDownX - event.getX() > getWidth() / 2) {
					// 手指離開點的X軸座標 - firstDownX < 屏幕寬度的一半,右移
					nextId = currId + 1;
				} else {
					nextId = currId;
				}

				moveToDest(nextId);
				break;
			default :
				break;
		}
		return true;
	}

	/**
	 * 控制視圖的移動
	 * 
	 * @param nextId
	 */
	private void moveToDest(int nextId) {
		// nextId的合理範圍是,nextId >=0 && nextId <= getChildCount()-1
		currId = (nextId >= 0) ? nextId : 0;
		currId = (nextId <= getChildCount() - 1)
				? nextId
				: (getChildCount() - 1);

		// 視圖移動,太直接了,沒有動態過程
		// scrollTo(currId * getWidth(), 0);
		// 要移動的距離 = 最終的位置 - 現在的位置
		int distanceX = currId * getWidth() - getScrollX();
		// 設置運行的時間
		myScroller.startScroll(getScrollX(), 0, distanceX, 0);
		// 刷新視圖
		invalidate();
	}

	/**
	 * invalidate();會導致這個方法的執行
	 */
	@Override
	public void computeScroll() {
		if (myScroller.computeOffset()) {
			int newX = (int) myScroller.getCurrX();
			System.out.println("newX::" + newX);
			scrollTo(newX, 0);
			invalidate();
		}
	}

}

        1,上面是自定義ViewGroup的所有源碼,接下來我們慢慢分析一下實現過程,首先是初始化各個子View的排版,上面已經說過了,主要代碼在onLayout()方法中已經體現,比較簡單。


        2,實現手勢滑動效果。衆所周知,ViewPager可以隨着手指在屏幕上滑動而改變不同的分頁,爲了實現同樣的效果,我在自定義ViewGroup中重寫了父類的onTouchEvent(MotionEvent event)方法,該方法被用來處理滑動事件的邏輯。但是爲了簡便起見,我用了手勢識別器GestureDetector,用這個手指識別器來處理手指在屏幕上移動時,視圖跟着手指一起移動的效果,簡單在GestureDetector的onScroll()方法中,將移動的距離傳遞給ScrollBy(int)作爲參數即可。


        3,處理比較複雜的手指按下到擡起時,視圖切換。這是一個具體分析的過程,下面是這個過程中涉及的"草圖":


這裏,我們以子View2這個View做示例來分析一下3種情況:

(1),手指離開點的X軸座標 - 手指按下點的X軸座標 > 屏幕寬度的一半,左移,屏幕顯示下一個View

(2),手指離開點的X軸座標 - 手指按下點的X軸座標 < 屏幕寬度的一半,右移,屏幕顯示上一個View

(3),以上兩種條件都不滿足,那就停留在當前View上,不切換前後View


       4,通過(3)的過程,我們就知道當前視圖向哪一個View方向上移動了,得到下一個需要顯示View的id,將這個id置爲當前View的id,然後將下一個需要顯示的View的id*View的寬度,傳遞給ScrollTo(int,0)作爲參數,來控制視圖的移動。


       5,通過以上步驟,View視圖的切換就已經完成了,但是有個問題,在View的左右切換時使用了ScrollTo(int,int)方法,這個方法將View直接移動到指定的位置,但是整個移動的過程太過於迅速,一瞬間就完成了View的切換,這樣的體驗效果非常差,那麼我們怎麼提升體驗效果呢?對了,是在這個View的切換給一個慢速的過程,讓View切換的過程緩慢或者勻速的進行,這樣體驗效果就提生上去了,那麼怎樣在切換的過程中增加一個勻速的切換的效果呢?我們不妨先舉下面一個小例子,方便理解:


       假如,有個人小A要走完一個100米的小路,他自己可以慢慢的走過去,用時很多,也可以一下子跑過去,用時極短,但是他想不緊不慢的勻速走完這段小路,該怎麼辦呢?這時候他找來了一位工程師小B,讓工程師小B在旁邊幫他計算路程,小A在前進前詢問一下工程師小B,接下來5秒鐘,我要走多少米啊?工程師小B就開始計算出結果,並且告訴小A,你先前進10米好了;當小A走完這個10米的路程時,小A又問小B,接下來5秒鐘我要前進多少米的距離?小B一頓計算,告訴小A前進20米好了,於是小A繼續前進20米,停下來接着問小B......反覆此過程,知道小A走完這100米的小路爲止。


       上面的例子不難理解吧!於是,在View的切換過程中,我們也需要這樣的一位“工程師”時刻計算每一定時間間隔內的位移,傳遞給View視圖,視圖得到這個位移,就立馬移動到相應的位置,再次請求“工程師”計算下,下一時間間隔內前進的位移,以此類推。下面,是我們自定義的一個計算位移的工具類源碼:

package com.example.myviewpager;

import android.content.Context;
import android.os.SystemClock;

/**
 * 計算視圖偏移的工具類
 * 
 * @author Administrator
 * 
 */
public class MyScroller {

	/** 開始時的X座標 */
	private int startX;
	/** 開始時的Y座標 */
	private int startY;
	/** X方向上要移動的距離 */
	private int distanceX;
	/** Y方向上要移動的距離 */
	private int distanceY;
	/** 開始的時間 */
	private long startTime;
	/** 移動是否結束 */
	private boolean isFinish;
	/** 當前X軸的座標 */
	private long currX;
	/** 當前Y軸的座標 */
	private long currY;
	/** 默認的時間間隔 */
	private int duration = 500;

	public MyScroller(Context ctx) {

	}

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

	/**
	 * 判斷當前運行狀態
	 * 
	 * @return
	 */
	public boolean computeOffset() {

		if (isFinish) {
			return false;
		}
		// 獲得所用的時間
		long passTime = SystemClock.uptimeMillis() - startTime;
		System.out.println("passTime::" + passTime);
		// 如果時間還在允許的範圍內
		if (passTime < duration) {
			currX = startX + distanceX * passTime / duration;
			currY = startY + distanceY * passTime / duration;
		} else {
			currX = startX + distanceX;
			currY = startY + distanceY;
			isFinish = true;
		}

		return true;
	}

	/**
	 * 獲取當前X的值
	 * 
	 * @return
	 */
	public long getCurrX() {
		return currX;
	}

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

	/**
	 * 獲取當前Y的值
	 * 
	 * @return
	 */
	public long getCurrY() {
		return currY;
	}

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

}

分析一下,這個過程。


       當我們在計算出切換到下一個View的id時,就可以得到切換的距離了,公式:要移動的距離 = 最終的位置 - 現在的位置;得到這個移動距離之後,拿到這個距離和初始位置,告訴“工程師”——工具類MyScroller,這時候可以開始計算了,初始化代碼如下:

// 要移動的距離 = 最終的位置 - 現在的位置
int distanceX = currId * getWidth() - getScrollX();
// 設置運行的時間
myScroller.startScroll(getScrollX(), 0, distanceX, 0);
// 刷新視圖
invalidate();
       初始化完計算工具類之後,需要刷新當前視圖了,調用invalidate()方法,這個方法會經過一系列連鎖反應,事實上刷新視圖是個很複雜的過程,這裏不講解了,一直直到觸發computeScroll()方法,此時,我們需要重寫父類的computeScroll()方法,在這個方法中,完成自己的一些操作:

/**
* invalidate();會導致這個方法的執行
*/
@Override
public void computeScroll() {
	if (myScroller.computeOffset()) {
		int newX = (int) myScroller.getCurrX();
		System.out.println("newX::" + newX);
		scrollTo(newX, 0);
		invalidate();
	}
}

       

       在這個方法裏,首先調用一下工具類計算位移的方法computeOffset()方法,該方法首先判斷一下視圖移動是否完成,若完成返回false,若沒有完成,先獲取運動的時間間隔,如果當前運動的時間間隔在總時間間隔duration之內,那麼通過時間間隔計算出這段時間間隔之後,視圖實際移動到的位置,公式是:開始位置+總的距離/總的時間*本段移動時間間隔,如果當前運動的時間間隔超出了總的時間間隔,那麼直接算出最後一次位置,公式:開始位置+移動距離。通過getCurrX得到本次位移的距離,即最新的位移距離,調用scrollTo(int,int)方法,移動視圖到新的位置。最後再次遞歸調用invalidate()刷新當前視圖,然後觸發computeScroll()方法,繼續上述步驟,直至超出規定的時間間隔,返回false後,視圖的位移過程結束。


      在佈局文件中這樣引用:

<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" >

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

</RelativeLayout>
     在MainActivity裏需要給這個自定義的組件初始化幾個View,爲了方便起見,我全部初始化了ImageView,每個ImageView設置不同的背景圖片:

package com.example.myviewpager;

import android.os.Bundle;
import android.widget.ImageView;
import android.app.Activity;

public class MainActivity extends Activity {

	private MyViewPager myViewPager;
	// 圖片資源
	private int[] imageRes = new int[]{R.drawable.a1, R.drawable.a2,
			R.drawable.a3, R.drawable.a4, R.drawable.a5, R.drawable.a6};

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		myViewPager = (MyViewPager) findViewById(R.id.myviewpager);

		ImageView view;
		for (int i = 0; i < imageRes.length; i++) {
			view = new ImageView(this);
			view.setBackgroundResource(imageRes[i]);
			myViewPager.addView(view);
		}
	}

}



        此外,在這個例子程序中我自定義了一個MyScroller工具類來計算位移大小了,感覺費時費力,作爲學習原理可行,但是實際開發中,可以使用Android爲我們提供了類似的、極其簡便的Helper類,可以使用這個Helper類來計算位移,這個類就是

android.widget.Scroller; 

以下是Scroller類的相關方法:  

mScroller.getCurrX()    //獲取mScroller當前水平滾動的位置  
mScroller.getCurrY()    //獲取mScroller當前豎直滾動的位置  
mScroller.getFinalX()   //獲取mScroller最終停止的水平位置  
mScroller.getFinalY()     //獲取mScroller最終停止的豎直位置  
mScroller.setFinalX(int newX)    //設置mScroller最終停留的水平位置,沒有動畫效果,直接跳到目標位置  
mScroller.setFinalY(int newY)    //設置mScroller最終停留的豎直位置,沒有動畫效果,直接跳到目標位置  
mScroller.startScroll(int startX, int startY, int dx, int dy)   //滾動,startX, startY爲開始滾動的位置,dx,dy爲滾動的偏移量  
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)    //滾動,startX, startY爲開始滾動的位置,dx,dy爲滾動的偏移量, duration爲完成滾動的時間
mScroller.computeScrollOffset()   //返回值爲boolean,true說明滾動尚未完成,false說明滾動已經完成。這是一個很重要的方法,通常放在View.computeScroll()中,用來判斷是否滾動是否結束。

       Scroller的具體使用實踐在我的前面博文中有用過,請移步Android自定義控件——側滑菜單查看相關源碼。


源碼請在這裏下載


發佈了58 篇原創文章 · 獲贊 24 · 訪問量 40萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章