在自定義 View 的時候常常會用到有關 View 的移動,用的比較多的估計是動畫,可是除了動畫還有沒有什麼方法能夠實現相同的效果呢?有,並且還有好幾種方法,這裏總結一下目前所瞭解到的有關 View 的移動方法。在開始以前先來看張圖:
android
- View 提供的獲取座標的方法:
getLeft():獲取到的是 View 自身的左邊到其父佈局左邊的距離;
getTop():獲取到的是 View 自身的頂邊到其父佈局頂邊的距離;
getRight():獲取到的是 View 自身的右邊到其父佈局左邊的距離;
getBottom():獲取到的是 View 自身的底邊到其父佈局頂邊的距離。- MotionEvent 提供的獲取座標的方法:
getX():獲取點擊事件距離控件左邊的距離,即視圖座標;
getY():獲取點擊事件距離控件頂邊的距離,即視圖座標;
getRawX():獲取點擊事件距離整個屏幕左邊的距離,即絕對座標;
getRawY():獲取點擊事件距離整個屏幕頂邊的距離,即絕對座標。
有了上面這些知識點,對於接下來的計算就容易不少了。此次的總結分爲如下方面:ide
若是對自定義 View 有必定的瞭解就會知道,在 View 的繪製過程當中會調用 onLayout() 方法來設置 View 的位置。那麼一樣能夠經過修改 View 的 left、top、right、bottom 四個屬性來控制 View 的位置。下面來看看用 layout() 怎麼實現 View 的移動:佈局
// 相對位置
int x = (int) event.getX();
int y = (int) event.getY();複製代碼
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;複製代碼
在 ACTION_MOVE 事件中計算偏移量,而後在 View 當前的 left、top、right、bottom 上加上偏移量,最後將相加的結果設置到 layout() 方法中:post
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offSetX = x - lastX;
int offSetY = y - lastY;
// 在 View 當前的left、top、right、bottom 基礎上加上偏移量
layout(getLeft() + offSetX,
getTop() + offSetY,
getRight() + offSetX,
getBottom() + offSetY);
break;複製代碼
這裏有一點須要提示一下:layout() 方法的參數順序是 left、top、right、bottom。通過上面的三個步驟,每次移動後 View 都會調用 layout() 方法對本身從新佈局,從而達到移動 View 的效果。
學習
@Override
public boolean onTouchEvent(MotionEvent event) {
// 相對位置
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offSetX = x - lastX;
int offSetY = y - lastY;
// 在當前的left、top、right、bottom 基礎上加上偏移量
layout(getLeft() + offSetX,
getTop() + offSetY,
getRight() + offSetX,
getBottom() + offSetY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}複製代碼
在上面的代碼中使用的是 getX()、getY() 方法來獲取觸摸點的座標值,即便用相對位置。自定義 View 的佈局代碼:動畫
<cn.ljuns.androidgrowing.practice.DragView android:id="@+id/dragView" android:layout_width="100dp" android:layout_height="100dp">
</cn.ljuns.androidgrowing.practice.DragView>複製代碼
那可不可使用 getRawX() 和 getRawY() 方法即絕對位置呢?答案是確定的,只須要在前面的基礎上修改一小部分代碼就能夠實現一樣的效果:this
@Override
public boolean onTouchEvent(MotionEvent event) {
// 絕對位置
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offSetX = rawX - lastX;
int offSetY = rawY - lastY;
// 在當前的left、top、right、bottom 基礎上加上偏移量
layout(getLeft() + offSetX,
getTop() + offSetY,
getRight() + offSetX,
getBottom() + offSetY);
// 從新設置座標
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}複製代碼
主要修改的是:一、在第1步獲取觸摸點座標的時候用 getRawX()和 getRawY() 代替 getX()和 getY() ;二、在第3步 ACTION_MOVE 事件中從新設置初始座標。至於爲何要在最後設置初始座標,根據一開始的圖本身比劃比劃就懂了。spa
其實根據方法名字很容易猜到這兩個方法的意思:左右的偏移、上下的偏移。那該怎麼用呢?也很簡單,不論是使用相對位置仍是絕對位置來計算偏移量,前面的第1步和第2步不變,只須要把 layout() 方法替換爲 offsetLeftAndRight() 和 offsetTopAndBottom() 就能夠了,即:.net
@Override
public boolean onTouchEvent(MotionEvent event) {
// 相對位置
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offSetX = x - lastX;
int offSetY = y - lastY;
// 同時對 left 和 right 進行偏移
offsetLeftAndRight(offSetX);
// 同時對 top 和 bottom 進行偏移
offsetTopAndBottom(offSetY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}複製代碼
這裏是用相對位置計算的偏移量,用絕對位置計算的便宜量也同樣能夠實現相同的效果,只要記住:在最後須要從新設置初始座標。3d
首先咱們要知道 LayoutParams 中保存了一個 View 的佈局參數,經過改變 LayoutParams 來動態修改一個佈局的位置參數也能夠實現前面的效果。前面的第一、2步依然不變,只需修改第3步:
@Override
public boolean onTouchEvent(MotionEvent event) {
// 相對位置
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offSetX = x - lastX;
int offSetY = y - lastY;
/** * LayoutParams 主要是經過修改 margin 來修改 view 的位置 */
LinearLayout.LayoutParams params =
(LinearLayout.LayoutParams) getLayoutParams();
params.leftMargin = getLeft() + offSetX;
params.topMargin = getTop() + offSetY;
setLayoutParams(params);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}複製代碼
在一個 View 中,系統提供了 scrollTo() 和 scrollBy() 兩種方法來改變一個 View 的位置。這兩個方法的區別是:scrollTo(x, y) 表示移動到一個具體的座標點(x, y);scrollBy(x, y) 表示移動的偏移量爲 x、y。與前面幾種方式相同,只需修改第3步的關鍵方法就能夠實現相同的效果:
@Override
public boolean onTouchEvent(MotionEvent event) {
// 相對位置
// int x = (int) event.getX();
// int y = (int) event.getY();
// 絕對位置
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// lastX = x;
// lastY = y;
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
// int offSetX = x - lastX;
// int offSetY = y - lastY;
int offSetX = rawX - lastX;
int offSetY = rawY - lastY;
((View)getParent()).scrollBy(-offSetX, -offSetY);
// ((View)getParent()).scrollTo(-offSetX, -offSetY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}複製代碼
懵逼了吧?爲毛是 ((View)getParent()).scrollBy(-offSetX, -offSetY)
而不是 scrollBy(offSetX, offSetY)
??爲毛是 (-offSetX, -offSetY)
??
第一個問題:由於 scrollTo() 和 scrollBy() 方法移動的是 View 的 content,即移動的是 View 的內容。例如 TextView 的 content 就是它的文本,因此若是要移動某個 View ,那麼就要在 View 所在的 ViewGroup 中使用 scrollTo()、scrollBy() 方法。明白了第一個問題,第二個問題也就迎刃而解了:由於 scrollTo()、scrollBy() 方法做用在 ViewGroup 上,因此要往反方向移動才能實現咱們須要的效果。
首先來看個效果圖:
當我鼠標鬆開時,藍色的矩形會平滑的移動,這是怎麼作到的呢?
因爲在 ACTION_MOVE 事件中不斷獲取手指移動的微小的偏移量,這樣就將一段距離劃分紅了 N 個很是小的偏移量,在每一個小的偏移量裏面經過調用 scrollTo() 方法進行了移動。由於人眼的視覺暫留特性,使得在總體上是一個平滑移動的效果。
這就是 Scroller ,接下來看看代碼是怎麼實現的:
// 初始化 Scroller
mScroller = new Scroller(context);複製代碼
/** * 核心方法,該方法是個空方法,實質是經過調用 scrollTo 實現移動 */
@Override
public void computeScroll() {
super.computeScroll();
// 判斷 Scroller 是否執行完畢
if (mScroller.computeScrollOffset()) {
((View)getParent()).scrollTo(
mScroller.getCurrX(),
mScroller.getCurrY());
// 經過重繪來不斷調用 computeScroll()
invalidate();
}
}複製代碼
computeScroll() 方法是使用Scroller 類的核心,系統在繪製 View 的時候會在 draw() 方法中調用該方法。Scroller 類提供了 computeScrollOffset() 方法來判斷是否完成了整個滑動,同時也提供了 getCurrX()、getCurrY() 方法來得到當前的滑動座標。使用 startScroll() 開啓平滑移動
View viewGroup = (View) getParent();
// 啓動
mScroller.startScroll(viewGroup.getScrollX(),
viewGroup.getScrollY(),
-viewGroup.getScrollX(),
-viewGroup.getScrollY(),
3000);
invalidate();複製代碼
這裏給它設置了一個時長:3000,是平滑移動的時長,固然也能夠省略。最重要的是 computeScroll() 方法不會自動調用,只能經過 invalidate() -> draw() -> computeScroll() 來間接調用 computeScroll() ,因此必定要在最後調用 invalidate() 方法。
總的執行流程是這樣的:startScroll() -> invalidate() -> draw() -> computeScroll() -> invalidate() -> draw() -> computeScroll()... 就這樣一直循環下去直到結束。整個自定義 View 的完整代碼:
public class DragView extends View {
private int lastX;
private int lastY;
private Scroller mScroller;
public DragView6(Context context) {
this(context, null);
}
public DragView6(Context context, AttributeSet attrs) {
super(context, attrs);
// 設置背景色
setBackgroundColor(Color.BLUE);
mScroller = new Scroller(context);
}
/** * 核心方法,該方法是個空方法,實質是經過調用 scrollTo 實現移動 */
@Override
public void computeScroll() {
super.computeScroll();
// 判斷 Scroller 是否執行完畢
if (mScroller.computeScrollOffset()) {
((View)getParent()).scrollTo(
mScroller.getCurrX(),
mScroller.getCurrY());
// 經過重繪來不斷調用 computeScroll
postInvalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 相對位置
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offSetX = x - lastX;
int offSetY = y - lastY;
((View)getParent()).scrollBy(-offSetX, -offSetY);
break;
case MotionEvent.ACTION_UP:
View viewGroup = (View) getParent();
// 啓動
mScroller.startScroll(viewGroup.getScrollX(),
viewGroup.getScrollY(),
-viewGroup.getScrollX(),
-viewGroup.getScrollY(),
3000);
invalidate();
break;
}
return true;
}
}複製代碼
以前寫了一篇屬性動畫的總結:學習總結--屬性動畫,用屬性動畫來實現 View 的移動會更簡單,先獲取到須要移動的 View ,而後給它設置動畫:
//屬性動畫
dragView = (DragView) findViewById(R.id.dragView);
ObjectAnimator animator1 = ObjectAnimator.ofFloat(dragView,
"translationX", 0, 200);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(dragView,
"translationY", 0, 200);
AnimatorSet set = new AnimatorSet();
set.playTogether(animator1, animator2);
set.setDuration(3000);
set.start();複製代碼
這裏是屬性動畫組合,View 會從座標 (0, 0) 平滑移動到座標 (200, 200),持續時長是3000ms(也就是3秒)。
在 support 庫中有 DrawerLayout 和 SlidingPaneLayout 兩個佈局能夠實現側邊欄的滑動,而它們的核心就是 ViewDragHelper 類,經過 ViewDragHelper 基本能夠實現各類不一樣的滑動、拖放的需求。依然是前面的效果:
// 初始化
mHelper = ViewDragHelper.create(this, callback);複製代碼
第一個參數是要監聽的 View,一般是一個 ViewGroup ;第二個參數是一個 Callback 回調。攔截事件
重寫 onInterceptTouchEvent() 和 onTouchEvent() 方法。若是不瞭解這兩個方法,得先去了解下 Android 的事件攔截機制。
/** * 事件攔截 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mHelper.shouldInterceptTouchEvent(ev);
}
/** * 事件處理 */
@Override
public boolean onTouchEvent(MotionEvent event) {
// 將觸摸事件傳遞給 ViewDragHelper
mHelper.processTouchEvent(event);
return true;
}複製代碼
@Override
public void computeScroll() {
if (mHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}複製代碼
computeScroll() 方法在前面 Scroller 類的時候有提到過,ViewGroupHelper 內部也是經過 Scroller 來實現平滑移動的。Callback 回調
private class HelperCallback extends ViewDragHelper.Callback{
/** * 檢測觸摸事件 */
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 若是當前觸摸的 View 是 mView 就開始檢測觸摸事件
return mView == child;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
}複製代碼
經過 tryCaptureView() 方法能夠指定哪個子 View 能夠移動;clampViewPositionVertical() 和 clampViewPositionHorizontal() 分別對應垂直和水平方向上的滑動。它們的默認返回值是0,即不滑動。
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mView = getChildAt(0);
}複製代碼
經過 getChildAt() 方法按順序來加載子 View。其實 ViewDragHelper 還有更多更復雜的用法,能夠實現更炫的效果,感興趣的能夠本身去搜索一下相關的文章。上面一共總結了7種方法能夠實現 View 的移動,這篇學習總結也就到這了。這是第二篇學習總結,接下來會繼續學習繼續總結。