Android開發藝術筆記 | View的滑動(三種普遍實現方式及其對比、實戰)
- 2020 年 3 月 26 日
- 筆記
滑動在Android開發中具有很重要的作用, 不管一些滑動效果多麼絢麗, 歸根結底,它們都是由不同的滑動外加一些特效所組成的。 因此,掌握滑動的方法是實現絢麗的自定義控制項的基礎。
- 常見的實現View的滑動的三種方式: 第一種是通過
View本身提供的scrollTo/scrollBy方法來實現滑動; 第二種是通過動畫給View施加平移效果來實現滑動; 第三種是通過改變View的LayoutParams使得View重新布局從而實現滑動。
The 1. 使用scrollTo/scrollBy
View提供了專門的方法來實現滑動, 即scrollTo()和scrollBy(),這兩個方法的實現如下:
/** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int,int,int,int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x,int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX,mScrollY,oldX,oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } /** * Move the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int,int,int,int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally * @param y the amount of pixels to scroll by vertically */ public void scrollBy(int x,int y) { scrollTo(mScrollX + x,mScrollY + y); }- 從以上源碼可看出,
scrollBy實際上也是調用了scrollTo方法, 它實現了基於當前位置的相對滑動, 而scrollTo則實現了基於所傳遞參數的絕對滑動。 - 這裡要注意
滑動過程中View內部的兩個屬性mScrollX和mScrollY的改變規則, 這兩個屬性可以通過getScrollX和getScrollY方法分別得到。 - 在滑動過程中,
mScrollX的值總是 等於View左邊緣和View內容左邊緣在水平方向的距離, (即mScrollX = View左邊緣的X值 - View內容左邊緣的X值) 而mScrollY的值總是 等於View上邊緣和View內容上邊緣在豎直方向的距離。 (即mScrollY = View上邊緣的Y值 - View內容上邊緣的Y值) View邊緣是指View的位置,由四個頂點組成, 而View內容邊緣是指View中的內容的邊緣, 【上一篇筆記(事件體系) 說了,頂點即View的位置資訊, 不改變View的布局參數LayoutParams,它們的值便不會變動!!! 所以,這裡的View邊緣即View在繪製時layout階段確定了的原始布局位置!!!】scrollTo和scrollBy只能改變View內容的位置即,本方式實現的是View 內容的滑動!!! 而不能改變View 本身在布局中的位置和頂點坐標!!! 也就是說,不管怎麼滑動, 也不可能將當前View滑動到附近View所在的區域;-
mScrollX和mScrollY的單位為像素, 並且當View左邊緣在View內容左邊緣的右邊時,mScrollX為正值, 反之為負值; 當View上邊緣在View內容上邊緣的下邊時,mScrollY為正值, 反之為負值。 換句話說, 如果從左向右滑動,那麼mScrollX為負值,反之為正值; 如果從上往下滑動,那麼mScrollY為負值,反之為正值。

綠色邊框代表View在螢幕上對應的矩形區域,灰色陰影代表View的內容
The 2. 使用動畫
- 通過動畫可以讓一個View進行平移,而平移就是一種滑動。
- 主要是操作
View的translationX和translationY屬性, 既可以採用傳統的View動畫,也可以採用屬性動畫; - 如果採用
屬性動畫的話, 為了能夠兼容3.0以下的版本,需要採用開源動畫庫nineoldandroids(http://nineoldandroids.com/)。 - View動畫實例,在100ms內將一個View從原始位置向右下角移動100個像素:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true" android:zAdjustment="normal" > <translate android:duration="100" android:fromXDelta="0" android:fromYDelta="0" android:interpolator="@android:anim/linear_interpolator" android:toXDelta="100" android:toYDelta="100" /> </set>
- 屬性動畫示例,將一個View在100ms內從原始位置向右平移100像素。
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
- 注意,
- View動畫是對View的影像做操作, 並不能真正改變View的位置參數,包括寬/高, 並且如果希望動畫後的狀態得以保留還必須將fillAfter屬性設置為true, 否則動畫完成後動畫結果會消失。
- 使用屬性動畫並不會存在上述問題, 但是在Android 3.0以下無法使用屬性動畫, 需使用動畫兼容庫nineoldandroids來實現屬性動畫, 不過, 在Android 3.0以下的手機上通過nineoldandroids來實現的屬性動畫 本質上仍然是View動畫。
- 同時View動畫還有一個很嚴重的問題, 比如我們通過View動畫將一個Button向右移動100px, 並且這個View設置的有單擊事件, 這樣子單擊新位置無法觸發onClick事件, 而單擊原始位置仍然可以觸發onClick事件, 儘管Button已經不在原始位置了。 它的位置資訊(四個頂點和寬/高)並不會隨著動畫而改變, 因此在系統眼裡,這個Button並沒有發生任何改變, 它的
真身仍然在原始位置, 在新位置上只是View的影像而已。 基於這一點, 我們不能簡單地給一個View做平移動畫 並且還希望它在新位置繼續觸發事件。
The 3. 改變布局參數
- 改變布局參數,即改變LayoutParams,使View
重新布局。 比如我們想把一個Button向右平移100px, 我們只需要將這個Button的LayoutParams里的marginLeft參數的值增加100px即可, 示例程式碼:
MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams(); params.width += 100; params.leftMargin += 100; mButton1.requestLayout();//或mButton1.setLayoutParams(params);
各種滑動效果實現方式的對比
The 1. scrollTo/scrollBy:
- View提供的原生方法,專門用於View的滑動,
- 操作簡單方便,
- 不影響內部元素的單擊事件。
- 缺點:它只能滑動View的內容,並不能滑動View本身。
The 2. 動畫:
- 使用動畫實現View的滑動,要分情況。
- Android 3.0以上並採用屬性動畫的方式, 沒有明顯的缺點; 適用於需具有交互性的View;
- 使用View動畫或者在Android 3.0以下使用屬性動畫, 則均不能改變View本身的屬性。 適用於View不需要響應交互的情況, 需要交互則不太適合。
- 優點:一些複雜的效果必須要通過動畫才能實現。
The 3. 改變布局參數
- 適用於需具有交互性的View;
- 缺點:操作稍微複雜;
實戰
- 一三種方式, 我們使用自定義View的方式來實現View的滑動, 主要是配置一下背景顏色,以及在onTouchEvent()中做一下事件處理; 這裡貼上關鍵程式碼,詳細請見GitHub項目地址;
The 1. scrollTo/scrollBy
public class DragView4 extends View { private int lastX; private int lastY; public DragView4(Context context) { super(context); ininView(); } public DragView4(Context context, AttributeSet attrs) { super(context, attrs); ininView(); } public DragView4(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); ininView(); } private void ininView() { setBackgroundColor(Color.BLUE); } @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = (int) event.getX(); lastY = (int) event.getY(); break; case MotionEvent.ACTION_MOVE: int offsetX = x - lastX; int offsetY = y - lastY; ((View) getParent()).scrollBy(-offsetX, -offsetY); break; } return true; } }The 2. 動畫
- 以上已經對View動畫、屬性動畫分別給過例子;
或參考以下部落格:
The 3. 改變布局參數
public class DragView3 extends View { private int lastX; private int lastY; public DragView3(Context context) { super(context); ininView(); } public DragView3(Context context, AttributeSet attrs) { super(context, attrs); ininView(); } public DragView3(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); ininView(); } private void ininView() { setBackgroundColor(Color.BLUE); } @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 記錄觸摸點坐標 lastX = (int) event.getX(); lastY = (int) event.getY(); break; case MotionEvent.ACTION_MOVE: // 計算偏移量 int offsetX = x - lastX; int offsetY = y - lastY; ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams(); // LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams); break; } return true; } }
