Android自定義ViewPager(一)――自定義Scroller模擬動畫過程
來源:程序員人生 發布時間:2014-12-10 08:32:22 閱讀次數:2535次
轉載請注明出處:http://blog.csdn.net/allen315410/article/details/41575831
相信Android SDK提供的ViewPager組件,大家實在是熟習不過了,但是ViewPager存在于support.v4包下的,說明ViewPager其實不存在于初期的android版本中,那末如何在初期的android版本中也一樣使用類似于ViewPager1樣的滑動效果呢?這里,我們還是繼續探討1下andrid的自定義組件好了,并且這篇博文只探討android的1些知識,其實不是刻意去構建1個自定義的ViewPager去使用,這個是沒有必要的,請將注意力集中在實現這個效果的知識點上,方便以后“舉1反3”。
好了,我們先來簡單分析1下ViewPager。ViewPager可以看作是1個“容器”,在這個“容器”里可以擺放各種各樣的View類型,例如ViewPager每一個分頁上可以放置TextView,ImageView,ListView、GridView等等1系列View組件,實際上這些View在ViewPager上的擺放我們可以看作是在ViewGroup上Layout各種View(實際上,這個實現是比較復雜的,這里做個比喻意義而已),所以我們就能夠抽象理解為,ViewPager相當于ViewGroup,并且在這個ViewGroup上Layout各種View,所以接下來的代碼中,我們主要需要1個自定義的ViewGroup來實現到達這樣的效果。另外,還需要在這個ViewGroup上給每一個分頁上的View添加1個左右滑動的效果,以求摹擬出ViewPager上的動態效果。
關于自定義ViewGroup的結構,我們有必要仔細探討1下,某些概念還是值得去加深理解的,為了理解方便,請參看下面的“草圖”:

從上面的草圖可以看到,紅色的邊框代表裝備屏幕,即我們可以用肉眼看見的地方,全部灰色的大邊框代表全部效果,這里稱為“視圖”,每一個視圖又分為3個View,這個3個或多個View組成1張很大的視圖。我們要弄清楚,這3者的關系,裝備屏幕代表的顯示區域,即我們在裝備上能看見的范圍,View代表的是單個的組件,1個屏幕上可以顯示1個或多個View,但是視圖是最容易混淆的東西,視圖理論上是很大的1塊區域,它不但包括裝備屏幕上能被肉眼看見的1部份,還包括裝備屏幕之外肉眼看不見的地方,就如上圖所示的,子View2和子View3也是視圖的1部份,但是在裝備屏幕以外,就是肉眼看不見的區域了。視圖里可以寄存很多的View,視圖被用來管理View的顯示效果。而且,視圖是可以自由活動的,通過控制視圖的活動,控制視圖在裝備屏幕上的顯示范圍,就能夠切換不同的分頁了。
所以接下來,我們主要去做的就是如何去自定義1個視圖,如何讓視圖展現不同的View在裝備屏幕上,在Android上管理多個View的顯示可以通過自定義的ViewGroup,實現onLayout給View進行排版,初始化排版的時候,我1共向ViewGroup里添加了6個子View,這6個子View呈水平橫向排版,如上圖所示的那樣,每一個View顯示的寬度和高度跟父View(ViewGroup)相同,首次排版顯現出第1個子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;
/** 第1次按下的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
指當前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); // 指定手勢辨認器去處理滑動事件
// 還是得自己處理1些邏輯
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN : // 按下
firstDownX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE : // 移動
break;
case MotionEvent.ACTION_UP : // 抬起
int nextId = 0; // 記錄下1個View的id
if (event.getX() - firstDownX > getWidth() / 2) {
// 手指離開點的X軸坐標-firstDownX > 屏幕寬度的1半,左移
nextId = (currId - 1) <= 0 ? 0 : currId - 1;
} else if (firstDownX - event.getX() > getWidth() / 2) {
// 手指離開點的X軸坐標 - firstDownX < 屏幕寬度的1半,右移
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()⑴
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的所有源碼,接下來我們漸漸分析1下實現進程,首先是初始化各個子View的排版,上面已說過了,主要代碼在onLayout()方法中已體現,比較簡單。
2,實現手勢滑動效果。盡人皆知,ViewPager可以隨著手指在屏幕上滑動而改變不同的分頁,為了實現一樣的效果,我在自定義ViewGroup中重寫了父類的onTouchEvent(MotionEvent event)方法,該方法被用來處理滑動事件的邏輯。但是為了簡便起見,我用了手勢辨認器GestureDetector,用這個手指辨認器來處理手指在屏幕上移動時,視圖隨著手指1起移動的效果,簡單在GestureDetector的onScroll()方法中,將移動的距離傳遞給ScrollBy(int)作為參數便可。
3,處理比較復雜的手指按下到抬起時,視圖切換。這是1個具體分析的進程,下面是這個進程中觸及的"草圖":

這里,我們以子View2這個View做示例來分析1下3種情況:
(1),手指離開點的X軸坐標 - 手指按下點的X軸坐標 > 屏幕寬度的1半,左移,屏幕顯示下1個View
(2),手指離開點的X軸坐標 - 手指按下點的X軸坐標 < 屏幕寬度的1半,右移,屏幕顯示上1個View
(3),以上兩種條件都不滿足,那就停留在當前View上,不切換前后View
4,通過(3)的進程,我們就知道當前視圖向哪個View方向上移動了,得到下1個需要顯示View的id,將這個id置為當前View的id,然后將下1個需要顯示的View的id*View的寬度,傳遞給ScrollTo(int,0)作為參數,來控制視圖的移動。
5,通過以上步驟,View視圖的切換就已完成了,但是有個問題,在View的左右切換時使用了ScrollTo(int,int)方法,這個方法將View直接移動到指定的位置,但是全部移動的進程太過于迅速,1瞬間就完成了View的切換,這樣的體驗效果非常差,那末我們怎樣提升體驗效果呢?對了,是在這個View的切換給1個慢速的進程,讓View切換的進程緩慢或勻速的進行,這樣體驗效果就提生上去了,那末怎樣在切換的進程中增加1個勻速的切換的效果呢?我們無妨先舉下面1個小例子,方便理解:
假設,有個人小A要走完1個100米的小路,他自己可以漸漸的走過去,用時很多,也能夠1下子跑過去,用時極短,但是他想不緊不慢的勻速走完這段小路,該怎樣辦呢?這時候候他找來了1位工程師小B,讓工程師小B在旁邊幫他計算路程,小A在前進前詢問1下工程師小B,接下來5秒鐘,我要走多少米啊?工程師小B就開始計算出結果,并且告知小A,你先前進10米好了;當小A走完這個10米的路程時,小A又問小B,接下來5秒鐘我要前進多少米的距離?小B1頓計算,告知小A前進20米好了,因而小A繼續前進20米,停下來接著問小B......反復此進程,知道小A走完這100米的小路為止。
上面的例子不難理解吧!因而,在View的切換進程中,我們也需要這樣的1位“工程師”時刻計算每定時間間隔內的位移,傳遞給View視圖,視圖得到這個位移,就立馬移動到相應的位置,再次要求“工程師”計算下,下1時間間隔內前進的位移,以此類推。下面,是我們自定義的1個計算位移的工具類源碼:
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;
}
}
分析1下,這個進程。
當我們在計算出切換到下1個View的id時,就能夠得到切換的距離了,公式:要移動的距離 = 終究的位置 - 現在的位置;得到這個移動距離以后,拿到這個距離和初始位置,告知“工程師”――工具類MyScroller,這時候候可以開始計算了,初始化代碼以下:
// 要移動的距離 = 終究的位置 - 現在的位置
int distanceX = currId * getWidth() - getScrollX();
// 設置運行的時間
myScroller.startScroll(getScrollX(), 0, distanceX, 0);
// 刷新視圖
invalidate();
初始化完計算工具類以后,需要刷新當前視圖了,調用invalidate()方法,這個方法會經過1系列連鎖反應,事實上刷新視圖是個很復雜的進程,這里不講授了,1直直到觸發computeScroll()方法,此時,我們需要重寫父類的computeScroll()方法,在這個方法中,完成自己的1些操作:
/**
* invalidate();會致使這個方法的履行
*/
@Override
public void computeScroll() {
if (myScroller.computeOffset()) {
int newX = (int) myScroller.getCurrX();
System.out.println("newX::" + newX);
scrollTo(newX, 0);
invalidate();
}
}
在這個方法里,首先調用1下工具類計算位移的方法computeOffset()方法,該方法首先判斷1下視圖移動是不是完成,若完成返回false,若沒有完成,先獲得運動的時間間隔,如果當前運動的時間間隔在總時間間隔duration以內,那末通過時間間隔計算出這段時間間隔以后,視圖實際移動到的位置,公式是:開始位置+總的距離/總的時間*本段移動時間間隔,如果當前運動的時間間隔超越了總的時間間隔,那末直接算出最后1次位置,公式:開始位置+移動距離。通過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);
}
}
}

另外,在這個例子程序中我自定義了1個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說明轉動已完成。這是1個很重要的方法,通常放在View.computeScroll()中,用來判斷是不是轉動是不是結束。
Scroller的具體使用實踐在我的前面博文中有用過,請移步Android自定義控件――側滑菜單查看相干源碼。
源碼請在這里下載
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈