由于手機螢幕尺寸有限,但是又經常需要在螢幕中顯示大量的内容,這就使得必須有部分内容顯示,部分内容隐藏。這就需要用一個Android中很重要的概念——滑動。滑動,顧名思義就是view從一個地方移動到另外一個地方,我們平時看到的各種很炫的移動效果,都是在基本的滑動基礎上加入一些動畫技術實作的。在Android中實作滑動的方式有多種,比如通過scrollTo/scrollBy,動畫位移,修改位置參數等。本文主要介紹通過scrollTo/scrollBy方式來實作View的滑動,并通過該方法來實作一個自定義PagerView。
前言
轉載請聲明,轉載自【https://www.cnblogs.com/andy-songwei/p/11213718.html】,謝謝!
由于手機螢幕尺寸有限,但是又經常需要在螢幕中顯示大量的内容,這就使得必須有部分内容顯示,部分内容隐藏。這就需要用一個Android中很重要的概念——滑動。滑動,顧名思義就是view從一個地方移動到另外一個地方,我們平時看到的各種很炫的移動效果,都是在基本的滑動基礎上加入一些動畫技術實作的。在Android中實作滑動的方式有多種,比如通過scrollTo/scrollBy,動畫位移,修改位置參數等。本文主要介紹通過scrollTo/scrollBy方式來實作View的滑動,并通過該方法來實作一個自定義PagerView。
本文的主要内容如下:
一、 scrollTo/scrollBy實際滑動的是控件的内容
這裡我們必須要先了解一個基本概念:使用scrollTo/scrollBy來實作滑動時,滑動的不是控件本身的位置,而是控件的内容。了解這一點,可以結合ScrollView控件,我們平時使用的使用會在xml布局檔案中固定ScrollView的大小和位置,這也是我們肉眼看到的資訊。但是如果我們左右/上下滑動滾動條,會發現裡面原來還“藏”了許多“風景”。控件就像一個窗戶,我們看到的隻有窗戶大小的内容,實際上窗戶中“另有乾坤”。就像下面這張圖顯示的一樣:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SM1MjNzATMwYTMtcDM0kDMxIDMxkTM3ATOxAjMtIDMwIzN08CX3ATOxAjMvwlMwAjM3QzLcd2bsJ2Lc12bj5ycn9Gbi52YugTMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
當我們手指在控件上滑動時,移動的其實是橙色部分表示的内容,而不是灰色部分表示的控件位置。
二、scrollBy實際上通過調用scrollTo來實作
scrollTo(int x, int y)方法的作用是:滑動到(x,y)這個坐标點,是一個絕對位置。
scrollBy(int x, int y)方法的作用是:在原來的位置上,水準方向向左滑動x距離,豎直方向向上滑動的y距離(滑動方向問題我們後面會詳細講),是一個相對位置。
這裡我們先看看這兩個函數的源碼:
1 //===========View.java=========
2 /**
3 * Set the scrolled position of your view. This will cause a call to
4 * {@link #onScrollChanged(int, int, int, int)} and the view will be
5 * invalidated.
6 * @param x the x position to scroll to
7 * @param y the y position to scroll to
8 */
9 public void scrollTo(int x, int y) {
10 if (mScrollX != x || mScrollY != y) {
11 int oldX = mScrollX;
12 int oldY = mScrollY;
13 mScrollX = x;
14 mScrollY = y;
15 invalidateParentCaches();
16 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
17 if (!awakenScrollBars()) {
18 postInvalidateOnAnimation();
19 }
20 }
21 }
22
23 /**
24 * Move the scrolled position of your view. This will cause a call to
25 * {@link #onScrollChanged(int, int, int, int)} and the view will be
26 * invalidated.
27 * @param x the amount of pixels to scroll by horizontally
28 * @param y the amount of pixels to scroll by vertically
29 */
30 public void scrollBy(int x, int y) {
31 scrollTo(mScrollX + x, mScrollY + y);
32 }
注釋中也說明了這兩個方法的功能,也可以看到scrollBy,就是調用的scrollTo來實作的,是以實際上這兩個方法功能一樣,實際開發中看那個友善就用那個。這部分源碼邏輯比較簡單,這裡就不啰嗦了,需要注意的是mScrollX/mScrollY這兩個變量,後面會用到,它們表示目前内容已經滑動的距離(向左/上滑動為正,向右/下滑動為負,方向問題下面詳細講)。
三、滑動坐标系和View坐标系正好相反
上面一節中介紹過,内容向左/上滑動時mScrollX/mScrollY為正,向右/下滑動時為負,這似乎和我們所了解的正好相反。我們平時了解的是基于View的坐标系,水準向右為X軸正方向,豎直向下為Y軸正方向。但是滑動坐标系和View坐标系正好相反,對于滑動而言,水準向左為X軸正方向,豎直向上為Y軸正方向,原點都還是View控件的左上角頂點。如下圖所示:
僅從數值上看,mScrollX表示控件内容左邊緣到控件左邊緣的偏移距離,mScrollY表示控件内容上邊緣的距離與控件上邊緣的偏移距離。在實際開發中,經常通過getScrollX()/getScrollY()來擷取mScrollX/mScrollY的值。
1 //===========View.java=========
2 public final int getScrollX() {
3 return mScrollX;
4 }
5 ......
6 public final int getScrollY() {
7 return mScrollY;
8 }
對于其值的正負問題,讀者可以自己通過列印log的方式來示範一下,比較簡單,此處不贅述了。這裡再提供幾個圖來體會一下滑動方向的問題。
水準方向的滑動
豎直方向的滑動
四、通過Scroller實作彈性滑動
通過scrollTo/scrollBy實作滑動時,是一瞬間來實作的。這樣看起來會比較生硬和突兀,使用者體驗顯然是不友好的,很多場景下,我們希望這個滑動是一個漸近式的,在給定的一段時間内緩慢移動到目标坐标。Android提供了一個Scroller類,來輔助實作彈性滑動,至于它的使用方法,下一點的代碼中有詳細示範,紅色加粗的文字部分顯示了使用步驟,這裡結合該示例進行講解。
通過Scroller實作彈性滑動的基本思想是,将一整段的滑動分為很多段微小的滑動,并在一定時間段内一一完成。
我們來看看CustomPagerView中第111行startScroll方法的源碼:
1 //===================Scroller.java==================
2 /**
3 * Start scrolling by providing a starting point, the distance to travel,
4 * and the duration of the scroll.
5 *
6 * @param startX Starting horizontal scroll offset in pixels. Positive
7 * numbers will scroll the content to the left.
8 * @param startY Starting vertical scroll offset in pixels. Positive numbers
9 * will scroll the content up.
10 * @param dx Horizontal distance to travel. Positive numbers will scroll the
11 * content to the left.
12 * @param dy Vertical distance to travel. Positive numbers will scroll the
13 * content up.
14 * @param duration Duration of the scroll in milliseconds.
15 */
16 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
17 mMode = SCROLL_MODE;
18 mFinished = false;
19 mDuration = duration;
20 mStartTime = AnimationUtils.currentAnimationTimeMillis();
21 mStartX = startX;
22 mStartY = startY;
23 mFinalX = startX + dx;
24 mFinalY = startY + dy;
25 mDeltaX = dx;
26 mDeltaY = dy;
27 mDurationReciprocal = 1.0f / (float) mDuration;
28 }
startScroll方法實際上沒有做移動的操作,隻是提供了本次完整滑動的開始位置,需要滑動的距離,以及完成這次滑動所需要的時間。
第113行的invalidate()方法會讓CustomPagerView重繪,這會調用View中的draw(...)方法,
1 boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
2 ......
3 computeScroll();
4 ......
5 }
6 ......
7 /**
8 * Called by a parent to request that a child update its values for mScrollX
9 * and mScrollY if necessary. This will typically be done if the child is
10 * animating a scroll using a {@link android.widget.Scroller Scroller}
11 * object.
12 */
13 public void computeScroll() {
14 }
draw()方法調用了computeScroll(),這是一個空方法,在CustomPagerView的126行重寫了該方法,重繪時會進入到這個方法體中。第127行中有一個判斷條件,看看它的源碼:
1 /**
2 * Call this when you want to know the new location. If it returns true,
3 * the animation is not yet finished.
4 */
5 public boolean computeScrollOffset() {
6 if (mFinished) {
7 return false;
8 }
9
10 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
11
12 if (timePassed < mDuration) {
13 switch (mMode) {
14 case SCROLL_MODE:
15 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
16 mCurrX = mStartX + Math.round(x * mDeltaX);
17 mCurrY = mStartY + Math.round(x * mDeltaY);
18 break;
19 case FLING_MODE:
20 ......
21 break;
22 }
23 }
24 else {
25 mCurrX = mFinalX;
26 mCurrY = mFinalY;
27 mFinished = true;
28 }
29 return true;
30 }
這個判斷語句是在判斷本次滑動是否在在繼續,如果還沒結束,會傳回false,重寫的computeScroll()中第130~135行會繼續執行,直到滑動完成為止。同時這個方法還會根據已經滑動的時間來更新目前需要移動到位置mCurrX/mCurrY。是以我們可以看到,在滑動還沒結束時,第134行就執行scrollTo方法來滑動一段距離。第134行又是一個重新整理,讓CustomPagerView重繪,又會調用draw(...)方法,computeScroll方法又被調用了,這樣反複調用,直到整個滑動過程結束。(至于多長時間會執行一直重新整理,筆者目前還沒找到更深入的代碼,有興趣的讀者可以自己再深入研究研究)
最後這裡做個總結,Scroller輔助實作彈性滑動的原理為: Scroller本身不能實作滑動,而是通過startScroll方法傳入起始位置、要滑動的距離和執行完滑動所需的時間,再通過invalidate重新整理界面來調用重寫的computeScroll方法,在沒有結束滑動的情況下,computeScroll中執行scrollTo方法來滑動一小段距離,并再次重新整理界面調用重寫的computeScroll方法,如此反複,直到滑動過程結束。
五、實作一個自定義PagerView
本示例結合了該系列前面文章中提到的自定義View,View的繪制流程,觸摸事件處理,速度等方面的知識,不明白的可以先去看看這些文章,打一下基礎。本示例的項目結構非常簡單,這裡就不提供下載下傳位址了。
這裡先看看效果,一睹為快吧。
自定義一個view,繼承自ViewGroup
1 public class CustomPagerView extends ViewGroup {
2
3 private static final String TAG = "songzheweiwang";
4 private Scroller mScroller;
5 private VelocityTracker mVelocityTracker;
6 private int mMaxVelocity;
7 private int mCurrentPage = 0;
8 private int mLastX = 0;
9 private List<Integer> mImagesList;
10
11 public CustomPagerView(Context context, @Nullable AttributeSet attrs) {
12 super(context, attrs);
13 init(context);
14 }
15
16 private void init(Context context) {
17 //第一步:執行個體化一個Scroller執行個體
18 mScroller = new Scroller(context);
19 mVelocityTracker = VelocityTracker.obtain();
20 mMaxVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
21 Log.i(TAG, "mMaxVelocity=" + mMaxVelocity);
22 }
23
24 //添加需要顯示的圖檔,并顯示
25 public void addImages(Context context, List<Integer> imagesList) {
26 if (imagesList == null) {
27 mImagesList = new ArrayList<>();
28 }
29 mImagesList = imagesList;
30 showViews(context);
31 }
32
33 private void showViews(Context context) {
34 if (mImagesList == null) {
35 return;
36 }
37 for (int i = 0; i < mImagesList.size(); i++) {
38 ImageView imageView = new ImageView(context);
39 LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
40 imageView.setLayoutParams(params);
41 imageView.setBackgroundResource(mImagesList.get(i));
42 addView(imageView);
43 }
44 }
45
46 @Override
47 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
48 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
49 int count = getChildCount();
50 for (int i = 0; i < count; i++) {
51 View childView = getChildAt(i);
52 childView.measure(widthMeasureSpec, heightMeasureSpec);
53 }
54 }
55
56 @Override
57 protected void onLayout(boolean changed, int l, int t, int r, int b) {
58 int count = getChildCount();
59 for (int i = 0; i < count; i++) {
60 View childView = getChildAt(i);
61 childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
62 }
63 }
64
65 @Override
66 public boolean onTouchEvent(MotionEvent event) {
67 mVelocityTracker.addMovement(event);
68 int x = (int) event.getX();
69 switch (event.getActionMasked()) {
70 case MotionEvent.ACTION_DOWN:
71 //如果動畫沒有結束,先停止動畫
72 if (!mScroller.isFinished()) {
73 mScroller.abortAnimation();
74 }
75 mLastX = x;
76 break;
77 case MotionEvent.ACTION_MOVE:
78 int dx = x - mLastX;
79 //滑動坐标系正好和View坐标系是反的,dx為負數表示向右滑,為正表示向左滑
80 scrollBy(-dx, 0);
81 mLastX = x;
82 break;
83 case MotionEvent.ACTION_UP:
84 mVelocityTracker.computeCurrentVelocity(1000);
85 int xVelocity = (int) mVelocityTracker.getXVelocity();
86 Log.i(TAG, "xVelocity=" + xVelocity);
87 if (xVelocity > mMaxVelocity && mCurrentPage > 0) {
88 //手指快速右滑後擡起,且目前頁面不是第一頁
89 scrollToPage(mCurrentPage - 1);
90 } else if (xVelocity < -mMaxVelocity && mCurrentPage < getChildCount() - 1) {
91 //手指快速左滑後擡起,且目前頁面不是最後一頁
92 scrollToPage(mCurrentPage + 1);
93 } else {
94 slowScrollToPage();
95 }
96 break;
97 }
98 return true;
99 }
100
101 private void scrollToPage(int pageIndex) {
102 mCurrentPage = pageIndex;
103 if (mCurrentPage > getChildCount() - 1) {
104 mCurrentPage = getChildCount() - 1;
105 }
106 int scrollX = getScrollX();
107 int dx = mCurrentPage * getWidth() - scrollX;
108 int duration = Math.abs(dx) * 2;
109 Log.i(TAG, "[scrollToPage]scrollX=" + scrollX + ";dx=" + dx + ";duration=" + duration);
110 //第二步:調用startScroll方法,指定起始坐标,目的坐标和滑動時長
111 mScroller.startScroll(scrollX, 0, dx, 0, duration);
112 //第三步:讓界面重繪
113 invalidate();
114 }
115
116 private void slowScrollToPage() {
117 int scrollX = getScrollX();
118 //緩慢滑動式,滑動一半以上後自動換到下一張,滑動不到一半則還原
119 int whichPage = (scrollX + getWidth() / 2) / getWidth();
120 Log.i(TAG, "[slowScrollToPage]scrollX=" + scrollX + ";whichPage=" + whichPage);
121 scrollToPage(whichPage);
122 }
123
124 //第四步:重寫computeScroll方法,在該方法中通過scrollTo方法來完成滑動,并重繪
125 @Override
126 public void computeScroll() {
127 boolean isAnimateRun = mScroller.computeScrollOffset();
128 Log.i(TAG, "[computeScroll]isAnimateRun=" + isAnimateRun);
129 if (isAnimateRun) {
130 //目前頁面的右上角,相對于第一頁右上角的坐标
131 int curX = mScroller.getCurrX();
132 int curY = mScroller.getCurrY();
133 Log.i(TAG, "[computeScroll]curX=" + curX + ";curY=" + curY);
134 scrollTo(curX, curY);
135 postInvalidate();
136 }
137 }
138
139 @Override
140 protected void onDetachedFromWindow() {
141 super.onDetachedFromWindow();
142 if (mVelocityTracker != null) {
143 mVelocityTracker.recycle();
144 mVelocityTracker = null;
145 }
146 }
147 }
代碼看起來有點長,其實邏輯很簡單。基本思路是,使用者添加要顯示的圖檔資源id清單,在CustomPagerView中為每一個要顯示的圖檔執行個體一個ImageView進行顯示。在滑動的過程中,如果速度比較快(大于某個門檻值),手指擡起後,就會滑動下一頁。如果速度很慢,手指擡起時,如果手指滑動的距離超過了螢幕的一半,則自動滑到下一頁,如果沒滑到一半,本次就不翻頁,仍然停留在本頁。
在布局檔案中引入該控件
1 //=========activity_scroller_demo.xml=========
2 <?xml version="1.0" encoding="utf-8"?>
3 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"
6 android:orientation="vertical">
7
8 <com.example.demos.customviewdemo.CustomPagerView
9 android:id="@+id/viewpager"
10 android:layout_width="match_parent"
11 android:layout_height="300dp" />
12 </LinearLayout>
在Activity中使用該控件
1 public class ScrollerDemoActivity extends AppCompatActivity {
2
3 private static final String TAG = "ScrollerDemoActivity";
4
5 @Override
6 protected void onCreate(Bundle savedInstanceState) {
7 super.onCreate(savedInstanceState);
8 setContentView(R.layout.activity_scroller_demo);
9 initViews();
10 }
11
12 private void initViews() {
13 List<Integer> mImageList = new ArrayList<>();
14 mImageList.add(R.drawable.dog);
15 mImageList.add(R.drawable.test2);
16 mImageList.add(R.drawable.test3);
17 mImageList.add(R.drawable.test4);
18 CustomPagerView customPagerView = findViewById(R.id.viewpager);
19 customPagerView.addImages(this, mImageList);
20 }
21 }
這裡再啰嗦一句,本示例很好地示範了一個自定義View的開發,包含了不少自定義View需要掌握的基礎知識點。通過該代碼,希望能夠強化了解前面文章中介紹的相關知識。
六、其他實作滑動及彈性滑動的方法
前面隻介紹了通過scrollTo/scrollBy,并結合Scroller來實作滑動和彈性滑動的方式,實際上還有很多方式來實作這些效果。比如,要實作滑動,還有使用動畫以及修改控件位置參數等方式。要實作彈性滑動,已經知道了基本思路是把一整段滑動分為很多小段滑動來一一實作,那麼還可以使用定時器,Handler,Thread/sleep等方式來實作。這些方法就不一一介紹了,在使用時可以根據實際的場景和需求選擇實作方式。
結語
本文主要介紹通過scrollTo/scrollBy來實作控件内容的滑動,以及結合Scroller實作彈性滑動的方式。由于筆者水準和經驗有限,有描述不準确或不正确的地方,歡迎來拍磚,謝謝!
參考資料
《Android開發藝術探索》
【Android Scroller詳解】