有了之前自定義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:終點動作封裝;參數3:x方向的移動距離;參數4:y方向滑動距離 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+"-------"); //參數1:x的起始值;參數2:y的起始值;參數3:x的偏移量;參數4:y的偏移量 //對於參數3:dx>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文章。
微信公衆號圖片: