預備知識
-
Android屏幕區域劃分
我們先看一副圖來了解一下Android屏幕的區域劃分,如下:
Android屏幕的區域劃分
通過上圖我們可以很直觀的看到Android對于屏幕的劃分定義。下面我們就給出這些區域里常用區域的一些坐標或者度量方式。如下:
//獲取屏幕區域的寬高等尺寸獲取
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int widthPixels = metrics.widthPixels;
int heightPixels = metrics.heightPixels;
//應用程序App區域寬高等尺寸獲取
Rect rect = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
//獲取狀態欄高度
Rect rect= new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rectangle.top;
//View布局區域寬高等尺寸獲取
Rect rect = new Rect();
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);
特別注意:上面這些方法最好在Activity的onWindowFocusChanged ()方法或者之后調運,因為只有這時候才是真正的顯示OK。
- Android坐標系、View坐標系、位置的獲取、距離的獲取和View寬度的獲取
在Android中,將屏幕最左上角的頂點作為Android坐標系的原點,從這個點向右是X軸正方向,從這個點向下是Y軸正方向。
在Android中,將View的左上角頂點作為View坐標系的原點,從這個點向右是X軸正方向,從這個點向下是Y軸正方向。
下面我們就來看看在上面兩種坐標系下位置的獲取、距離的獲取和View寬度的獲取 的方法。
1中我們分析了Android屏幕的劃分,可以發現我們平時開發的重點其實都在關注View布局區域,那么下面我們就來細說一下View區域常用的位置和距離。先看下面這幅圖:
通過上圖我們可以很直觀的給出View一些坐標相關的方法解釋,不過必須要明確的是上面這些方法必須要在layout之后才有效,如下:
View的靜態坐標方法 | 解釋 |
---|---|
getLeft() | 返回View自身左邊到父布局左邊的距離(返回值是mLeft) |
getTop() | 返回View自身頂邊到父布局頂邊的距離(返回值是mTop) |
getRight() | 返回View自身右邊到父布局左邊的距離(返回值是mRight) |
getBottom() | 返回View自身底邊到父布局頂邊的距離(返回值是mBottom) |
getX() | 返回值為getLeft()+getTranslationX(),當setTranslationX()時getLeft()不變,getX()變。 |
getY() | 返回值為getTop()+getTranslationY(),當setTranslationY()時getTop()不變,getY()變。 |
同時也可以看見上圖中給出了手指觸摸屏幕時MotionEvent提供的一些方法解釋,如下:
MotionEvent坐標方法 | 解釋 |
---|---|
getX() | 當前觸摸事件距離當前View左邊的距離 |
getY() | 當前觸摸事件距離當前View頂邊的距離 |
getRawX() | 當前觸摸事件距離整個屏幕左邊的距離 |
getRawY() | 當前觸摸事件距離整個屏幕頂邊的距離 |
下面我們來看看幾個和上面方法緊密相關的獲取View寬高的View方法。如下:
View寬高方法 | 解釋 |
---|---|
getWidth() | layout后有效,返回值是mRight-mLeft,一般會參考measure的寬度(measure可能沒用),但不是必須的。 |
getHeight() | layout后有效,返回值是mBottom-mTop,一般會參考measure的高度(measure可能沒用),但不是必須的。 |
getMeasuredWidth() | 返回measure過程得到的mMeasuredWidth值,供layout參考,或許沒用。 |
getMeasuredHeight() | 返回measure過程得到的mMeasuredHeight值,供layout參考,或許沒用。 |
上面解釋了自定義View時各種獲取寬高的一些方法,下面我們再來看看獲取View可見區域和頂點坐標的一些方法,不過這些方法需要在Activity的onWindowFocusChanged ()方法之后才能使用。如下圖:
下面我們就給出上面這幅圖涉及的View的一些坐標方法的結果,如下所示:
View的方法 | 上圖View1結果 | 上圖View2結果 | 結論描述 |
---|---|---|---|
getLocalVisibleRect() | (0, 0, 410, 100) | (0, 0, 410, 470) | 獲取View自身可見的坐標區域,坐標以自己的左上角為原點(0,0),另一點為可見區域右下角相對自己(0,0)點的坐標,其實View2當前height為550,可見height為470。 |
getGlobalVisibleRect() | (30, 100, 440, 200) | (30, 250, 440, 720) | 獲取View在屏幕絕對坐標系中的可視區域,坐標以屏幕左上角為原點(0,0),另一個點為可見區域右下角相對屏幕原點(0,0)點的坐標。 |
getLocationOnScreen() | (30, 100) | (30, 250) | 坐標是相對整個屏幕而言,Y坐標為View左上角到屏幕頂部的距離。 |
getLocationInWindow() | (30, 100) | (30, 250) | 如果為普通Activity則Y坐標為View左上角到屏幕頂部(此時Window與屏幕一樣大);如果為對話框式的Activity則Y坐標為當前Dialog模式Activity的標題欄頂部到View左上角的距離。 |
通過layout方法實現滑動
我們知道,在View進行繪制時,會調用onLayout方法來設置顯示的位置。同樣,可以通過修改View的mLeft, mTop, mRight, mBottom四個屬性來控制View的位置。實現代碼如下所示:
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點坐標
android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ", y = " + y);
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ", y = " + y);
int offsetX = x - mLastX;
int offsetY = y - mLastY;
// 在當前mLeft, mTop, mRight, mBottom的基礎上加上偏移量
layout(getLeft() + offsetX, getTop() + offsetY, getRight()
+ offsetX, getBottom() + offsetY);
break;
default:
break;
}
return true;
}
通過offsetLeftAndRight()與offsetTopAndBottom實現滑動
這兩個方法相當于系統提供了一個對左右、上下移動的API的封裝。與上面一樣,也是通過修改View的mLeft, mTop, mRight, mBottom四個屬性來控制View的位置,實現代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點坐標
android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ", y = " + y);
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ", y = " + y);
int offsetX = x - mLastX;
int offsetY = y - mLastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
default:
break;
}
return true;
}
通過LayoutParams實現滑動
LayoutParams保存了一個View的布局參數,因此可以在程序中,通過改變LayoutParams來動態地修改一個View的布局參數,從而達到改變View位置的效果。我們可以很方便的在程序中使用getLayoutParams()來獲取一個View的LayoutParams(注意必須在layout之后才可以獲取到)。實現代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點坐標
android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ", y = " + y);
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ", y = " + y);
int offsetX = x - mLastX;
int offsetY = y - mLastY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
default:
break;
}
return true;
}
通過ViewDragHelper實現滑動
Google在其support庫中為我們提供了DrawerLayout和SlidingPaneLayout兩個布局來幫助開發者實現側邊欄滑動的效果。這兩個新的布局大大方便了我們創建自己的滑動布局界面。然而,這兩個功能強大的布局背后隱藏著一個鮮為人知卻功能強大的類---ViewDragHelper。通過ViewDragHelper基本可以實現各種不同的滑動、拖放需求,因此此方法也是各種滑動解決方案中的終極絕招。
ViewDragHelper雖然功能強大,但其使用方法也是最復雜的。下面通過一個實例,來演示一下如何使用ViewDragHelper創建一個滑動布局,在這個例子中,準備實現類似QQ滑動側邊欄的效果,初始時顯示內容界面,當用戶手指滑動超過一定距離時,內容界面側滑顯示菜單界面,整個過程下圖所示:
實現代碼如下所示:
package com.cytmxk.test.scroll;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
/**
* Created by chenyang on 16/6/26.
*/
public class DragViewGroup extends FrameLayout {
private static final String TAG = DragViewGroup.class.getCanonicalName();
private ViewDragHelper mViewDragHelper = null;
private View mMenuView = null;
private View mMainView = null;
private int mMenuWidth;
public DragViewGroup(Context context) {
super(context);
initView();
}
public DragViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
//初始化ViewDragHelper,第一個參數是要監聽的View,通常需要是一個ViewGroup,
//即parentView;第二個參數是一個Callback回調,后面會做解釋。
mViewDragHelper = ViewDragHelper.create(this, callback);
}
//獲取菜單布局的寬度,之后可以根據菜單布局(mMenuView)的寬度處理滑動后的效果。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mMenuWidth = mMenuView.getMeasuredWidth();
Log.d(TAG, "onSizeChanged mMenuWidth = " + mMenuWidth);
}
//初始化菜單布局(mMenuView)和主布局(mMainView)
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//將觸摸事件傳遞給ViewDragHelper,此操作必不可少
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
//將觸摸事件傳遞給ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
// 何時開始檢測觸摸事件,通過這個方法,我們可以指定在創建ViewDragHelper時,
//參數parentView中的哪一個View可以被移動。
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果當前觸摸的child是mMainView時開始檢測,并且只有mMainView可以被移動
return mMainView == child;
}
// 觸摸到View后回調
@Override
public void onViewCaptured(View capturedChild,
int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
// 當拖拽狀態改變,比如idle,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
// 當位置改變的時候調用,常用與滑動時更改scale等
@Override
public void onViewPositionChanged(View changedView,
int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
// 處理水平滑動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
// 處理垂直滑動
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
// 拖動結束后調用
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
Log.d(TAG, "onViewReleased mMainView.getLeft() = " + mMainView.getLeft() + ", mMenuWidth = " + mMenuWidth);
//手指抬起后緩慢移動到指定位置
if (mMainView.getLeft() < mMenuWidth) {
//關閉菜單
//相當于Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
} else {
//打開菜單
mViewDragHelper.smoothSlideViewTo(mMainView, mMenuWidth, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
//由于ViewDragHelper內部是利用Scroller實現滑動的,所以利用computeScroll方法實現平滑滑動
@Override
public void computeScroll() {
super.computeScroll();
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
通過scrollTo和scrollBy實現滑動
- 在一個View中,系統提供了scrollTo、scrollBy兩種方式來改變一個View中初始可見內容的位置。這兩個方法的區別非常好理解,與英文中To和By的區別類似,scrollTo(x, y)表示讓View中初始可見內容的在水平方向偏移到點(- x, - y)(x大于零表示向左偏移,否者向右偏移; y大于零表示向上偏移,否者向右偏移),scrollBy(dx, dy)表示讓View中初始可見內容的在水平方向偏移dx(dx大于零表示向左偏移,否者向右偏移),在垂直方向偏移dy(dy大于零表示向上偏移,否者向右偏移)如下是這兩個方法的代碼實現:
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollY;
/**
* 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);
}
有上面的代碼可以得知mScrollX,mScrollY是用來保存View初始可見內容的偏移量。理解了mScrollX和mScrollY的用法,就不難理解getScrollX() 和getScrollY()。這兩個函數的源碼如下所示:
/**
* Return the scrolled left position of this view. This is the left edge of
* the displayed part of your view. You do not need to draw any pixels
* farther left, since those are outside of the frame of your view on
* screen.
*
* @return The left edge of the displayed part of your view, in pixels.
*/
public final int getScrollX() {
return mScrollX;
}
/**
* Return the scrolled top position of this view. This is the top edge of
* the displayed part of your view. You do not need to draw any pixels above
* it, since those are outside of the frame of your view on screen.
*
* @return The top edge of the displayed part of your view, in pixels.
*/
public final int getScrollY() {
return mScrollY;
}
-
舉例說明,如下圖所示(注意,圖中黃色矩形區域表示的是View,綠色虛線矩形為View中初始可見的內容。一般情況下兩者的大小一致,本文為了顯示方便,將虛線框畫小了一點。圖中的黃色區域的位置始終不變,發生偏移的是初始可見的內容。):
scrollTo(0, 100)的效果如下圖所示:
scrollTo(100, 100)的效果圖如下:
若函數中參數為負值,則子View的移動方向將相反:
通過Scroller實現滑動
上面舉例中通過scrollTo偏移View的初始可見內容是在瞬間完成的,這樣的效果會讓人感覺非常突兀。Google也想到了這一點,所以提供了Scroller類來模擬平滑滑動的效果。
Scroller類提供了startScroll方法來初始化一個模擬平滑滑動的過程,然后調用invalidate()方法,這個方法會導致View重繪,系統在繪制View的時候會在draw方法中調用computeScroll方法來實現模擬滑動,在computeScroll方法中通過調用Scroller的computeScrollOffset方法判斷是否完成了整個滑動,同時Scroller也提供了getCurrX、getCurrY來獲取當前滑動過程中 View初始可見內容 即將的偏移量,然后利用srcollTo方法實現偏移即可,然后執行invalidate方法實現循環調用computeScroll方法直到滑動結束。
- Scroller中相關API簡介如下:
mScroller.getCurrX() //獲取mScroller當前水平方向滑動過程中的位置
mScroller.getCurrY() //獲取mScroller當前豎直方向滑動過程中的位置
mScroller.getFinalX() //獲取mScroller最終停止滑動的水平位置
mScroller.getFinalY() //獲取mScroller最終停止滑動的豎直位置
mScroller.setFinalX(int newX) //設置mScroller最終停留的水平位置,沒有動畫效果,直接跳到目標位置
mScroller.setFinalY(int newY) //設置mScroller最終停留的豎直位置,沒有動畫效果,直接跳到目標位置
mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默認完成時間250ms
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)
//開始滑動,startX, startY為 View初始可見內容 開始滑動的位置(即mScrollX,mScrollY的值),dx,dy分別為水平方向和垂直方向的偏移量(dx大于零表示向左偏移,否者向右偏移;dy大于零表示向上偏移,否者向下偏移), duration為完成滾動的時間 。
mScroller.computeScrollOffset() //返回值為boolean,true說明滑動尚未完成,false說明滑動已經完成。這是一個很重要的方法,通常放在View.computeScroll()中,用來判斷是否滑動是否結束。
2 舉例如下:
public void moveToDest(int index) {
/*
* 對 index 進行判斷 ,確保 是在合理的范圍
* 即 index >=0 && index <=getChildCount()-1
*/
//確保 index>=0
index = index >= 0 ? index : 0;
//確保 currIndex<=getChildCount()-1
currIndex = index <= getChildCount() - 1 ? index : getChildCount() - 1;
if (null != mOnPagerChangeListener) {
mOnPagerChangeListener.OnPagerChange(currIndex);
}
myScroller.startScroll(getScrollX(), 0, currIndex * getWidth() - getScrollX(), 0, 500);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (myScroller.computeScrollOffset()) {
scrollTo(myScroller.getCurrX(), 0);
invalidate();
}
}
參考文檔
- Android應用坐標系統全面詳解
- Android群英傳