1. 程式人生 > >android 仿viewpager滑動效果自定義升級版

android 仿viewpager滑動效果自定義升級版

上篇講了下仿viewpager豎直滑動的效果,那篇部落格有的細節沒說,那個是滑動到哪介面顯示在那,我們一般是滑動的距離超過螢幕的一半就到下一個介面,而且就是實現了這個功能,還是會有一個問題,因為它是一瞬間完成的,所以從效果看起來體驗不是很好,就像一個動畫,如果在很短的時間內完成,你就看不出來有動畫的效果,因此滑動看起來要有效果,得是在一定距離內有個時間在,這樣看起來才有反彈的效果,今天先不用系統自帶的Scroller類,我們自己實現下,然後再用Scroller,這樣理解起來更深刻點,好的,現在開始寫程式碼,

新建一個android專案:Customviewpager

先定義一個Customviewpager類繼承ViewGroup,然後再佈局檔案中使用這個自定義的類

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
  >
    <zhi.more.customviewpager.view.CustomViewPager
        android:id="@+id/custom_view_pager"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
         />
</RelativeLayout>
MainActivity.java
public class MainActivity extends Activity {
	private CustomViewPager custom_view_pager;
	private int[] ids = {R.drawable.a1,R.drawable.a2,R.drawable.a3,R.drawable.a4,R.drawable.a5,R.drawable.a6};
	private List<ImageView> imageViews;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		DisplayUtil.init(this);
		custom_view_pager = (CustomViewPager) findViewById(R.id.custom_view_pager);
		initData();
	}
	private void initData() {
		imageViews = new ArrayList<>();
		for(int i=0;i<ids.length;i++){
			ImageView imageView  = new ImageView(this);
			imageView.setBackgroundResource(ids[i]);
			imageViews.add(imageView);
			custom_view_pager.addView(imageView);
		}
	}
}

我們主要講下CustomViewPager的實現,我們都知道要自定義ViewGroup的話,onLayout()方法是必須實現,它的意思是說父view指定子view的位置,我們是仿viewpager效果,所以寬度是全屏的,高度也是,protected void onLayout(boolean changed, int l, int t, int r, int b) 這裡面幾個引數都是父view傳遞過來的,我們可以通過改變佈局檔案中的寬和高 你再打印出來onLayout()方法中的四個值會發現 變了,這就驗證了onLayout()中的引數是父view傳遞過來的,如果要實現類似viewpager效果,那麼子view怎麼排放呢?也就是每個子view的位置該怎麼設定呢?請看下面的圖你就知道了:


通過上面的圖我想應該知道了它的座標點,只是我們螢幕有限超過螢幕外的座標點我們看不到,但是拖動的時候就可以看的見了,那麼在onLayout()方法中就是這麼寫了:

for(int i=0;i<getChildCount();i++){
				View childView = getChildAt(i);
				childView.layout(i*getWidth(), 0, (i+1)*getWidth(), getHeight());//這是水平方向滑動
			}
這只是幫我們把每個子view位置搞定了,但是還不能滑動啊,是的,android中view的滑動都是都是通過onTouchEvent()這個方法,它有個引數MotionEvent,這個類就封裝了我們手在螢幕上一系列的操作,比如按下(down),移動(move),離開螢幕(up)等,如果就是想在onTouchEvent()方法中去實現當手滑動到超過螢幕一半就滑動到下一頁這個邏輯,也能實現,但是比較麻煩,android提供了一個類GestureDetector,它封裝了我們手在螢幕上更多的操作,什麼雙擊螢幕,單擊螢幕,滑動什麼的,它的初始化也很簡單,

detector = new GestureDetector(context, listener);第一個引數是上下文,第二個引數是一個介面,關於我們手在螢幕上操作的回撥,他有很多方法,截圖如下:


根據自己的邏輯看使用哪個方法,我們是要實現滑動的效果,所以我們只要實現onScroll()方法即可,

detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener(){
			/**
			 * distanceX 在螢幕上要移動的距離 而不是座標
			 */
			@Override
			public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
				scrollBy((int)distanceX,0);
				return  true;
			}
		});
GestureDetector類是幫我們封裝了滑動的操作,但是具體的滑動還是要我們去實現,在這裡我們就要用到view提供的scrollBy方法了,它是實現在螢幕上滑動的距離,是疊加的,比如我們手一直往右邊滑動,這個distanceX從3,4,7,9,它是累加的,這樣我們手滑動到哪,view也就滑動到哪了,關於onScrollBy()返回為true,表示自己處理了這個事件,這個會涉及到事件傳遞,如果有什麼不懂的,關於這方面,可以到網上找找部落格看看,在這就不講了,

那麼我們onScroll()方法中的程式碼就是這樣了

detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener(){
			/**
			 * distanceX 在螢幕上要移動的距離 而不是座標
			 */
			@Override
			public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
				scrollBy((int)distanceX,0);
				return  true;
			}
		});
因為我們只在x軸上滑動,y軸就不用了,所以就為0,別忘記了記得把滑動事件,交給GestureDetector,所以在onTouchEvent()方法中:
	public boolean onTouchEvent(MotionEvent event) {
		detector.onTouchEvent(event);
		return true;

到這裡就實現了滑動的效果,雖然滑動效果是實現了,但是體驗不好,不好的地方在於滑動到哪就顯示到哪,因為一般的需求都是滑動超過螢幕的一半就顯示下一個圖,因此我們要實現這個效果,先簡單分析下這個邏輯該怎麼實現,其實就是要計算手指按下和離開螢幕之間的距離和螢幕寬度的一半進行對比,但是就是這樣還會遇到一個問題啊,比如我現在在第三個介面,我向左滑動距離超過螢幕一半,那麼這個時候就會移動到第二個介面,這個時候又用到了view給我們提供了一個方法,scrollTo(),這個方法是移動到指定的座標點,再看下之前的圖:


我手機是728*1280的解析度,如果到第二個介面,它的座標是(1440,0),y軸上的座標我們是寫死的,那麼怎麼計算x軸上的座標呢?這個時候就要想到我們介面上有6個子view了,滑動到第二個介面的時候不就是下標(2)*getWidth(720)=1440,這樣就ok了,因此我們要定義一個index變數記錄當前滑動到第幾個view了,OK,分析到此,程式碼就出來了:

@Override
	public boolean onTouchEvent(MotionEvent event) {
		detector.onTouchEvent(event);
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			startX = event.getX();
			break;
		case MotionEvent.ACTION_MOVE:
					
		    break;
		case MotionEvent.ACTION_UP:
			float endX = event.getX();
			if((startX-endX)>getWidth()/2){//回到下一個頁面
				index++;
			}else if((endX-startX)>getWidth()/2){//回到上一個介面
				index--;
			}
			moveTo(index);
			break;
		}
		return true;
	}

	/**
	 * 根據下標移動到指定的介面
	 * @param index
	 */
	private void moveTo(int index) {
		if(index<0){
			index=0;
		}else if(index>getChildCount()-1){
			index = getChildCount()-1;
		}
		int x = index*getWidth();
		Log.e(TAG,"x="+x);
		scrollTo(x, 0);
	}
到目前為止我們實現了當滑動到超過螢幕的一半會回彈的問題,但是這樣體驗非常不好,因為我們是在瞬間完成的,這個也需要改動了,再分析下這個邏輯怎麼實現:

比如從(0,0)座標點,移動到(10,0)座標點,是移動了10個畫素點的距離,我可以人為的給它設定一個時間為500毫秒,那麼它的速度就是10/500了,現在我把這從(0,0)到(10,0)這個過程分為10等分的話是不是看起來就不一樣,體驗更好點,這樣速度就求出來了,速度怎麼求,度過小學的都知道,那麼如果我從(0,0)移動到(1,0)這個距離就為1了,但是我們滑動算的是座標,那麼就是開始位置0到滑動的距離1二者向加就是移動後的座標了,好了,邏輯我們分析完了,現在動手寫程式碼,我們可以新建一個工具類,專門維護這段邏輯

MyScroller.java



public class MyScroller {
private float startX;//x軸移動的的起始座標
private float startY;
private int distanceX;//x軸要移動的距離
private int distanceY;
private long startTime;//手機的開機時間
private boolean isFinish;//是否移動完成
private long totalTime = 300;//移動這個動畫所需要的總時間
private float currX;
public void startScroll(float startX ,float startY,int distanceX,int distanceY){
this.startX = startX;
this.startY = startY;
this.distanceX = distanceX;
this.distanceY = distanceY;
this.startTime = SystemClock.uptimeMillis();
this.isFinish = false;
}
/**
* 計算偏移量
* 移動一小段的時間
* 移動一小段的距離
* 移動一小段對應的座標
* 移動的平均速度
* true 表示正在移動 false表示移動結束
*/
public boolean computeScrollOffSet(){
if(isFinish){//表示移動結束
return false;
}
long endTime = SystemClock.uptimeMillis();
long passTime = endTime-startTime;
if(passTime<totalTime){//表示還在移動
//還在移動
//float volecityX = distanceX / totalTime;
//距離 = 時間* 速度
//這一小段的距離
float distanceSmallX  =  passTime * distanceX / totalTime;
//移動一小段轉換對應的座標
currX = startX + distanceSmallX  ;
}else{//表示移動結束了
currX = startX + distanceX;
isFinish  = true;
}
return true;
}
public float getCurrX() {
return currX;
}
}
上面的程式碼就是根據剛才分析的邏輯完成的,現在就用一下:

我們之前是用scrollTo()完成的,現在我們這麼呼叫:

貼一下主要的程式碼,因為上面都寫過了:

/**
* 根據下標移動到指定的介面
* @param index
*/
private void moveTo(int index) {
if(index<0){
index=0;
}else if(index>getChildCount()-1){
index = getChildCount()-1;
}
int x = index*getWidth();
//要移動的總距離
int distanceX = x-getScrollX();
//scrollTo(x, 0);
myScroller.startScroll(getScrollX(), 0, distanceX, 0);
invalidate();
}

我們都知道呼叫invalidate();函式會導致onDraw()介面重繪,但是還由於一個方法也會因為呼叫了invalidate()會被每次都會調,就是computeScroll()方法了,

@Override
public void computeScroll() {
super.computeScroll();
if(myScroller.computeScrollOffSet()){
float currX = myScroller.getCurrX();
scrollTo((int)currX,0);
invalidate();
}
}

這就是每次移動到指定的座標點,現在畫一個圖更好理解:


如果不使用我們寫的MyScroll類,系統也給我們提供了一個類叫Scroller,和我們寫的一樣,使用也一樣!ok到此結束