預備知識
-
Android屏幕區(qū)域劃分
我們先看一副圖來了解一下Android屏幕的區(qū)域劃分实愚,如下:
通過上圖我們可以很直觀的看到Android對于屏幕的劃分定義丰歌。下面我們就給出這些區(qū)域里常用區(qū)域的一些坐標或者度量方式梅肤。如下:
//獲取屏幕區(qū)域的寬高等尺寸獲取
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int widthPixels = metrics.widthPixels;
int heightPixels = metrics.heightPixels;
//應用程序App區(qū)域?qū)捀叩瘸叽绔@取
Rect rect = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
//獲取狀態(tài)欄高度
Rect rect= new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rectangle.top;
//View布局區(qū)域?qū)捀叩瘸叽绔@取
Rect rect = new Rect();
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);
特別注意:上面這些方法最好在Activity的onWindowFocusChanged ()方法或者之后調(diào)運掩缓,因為只有這時候才是真正的顯示OK筛武。
- Android坐標系桨仿、View坐標系报腔、位置的獲取偎谁、距離的獲取和View寬度的獲取
在Android中,將屏幕最左上角的頂點作為Android坐標系的原點俺驶,從這個點向右是X軸正方向幸逆,從這個點向下是Y軸正方向棍辕。
在Android中,將View的左上角頂點作為View坐標系的原點还绘,從這個點向右是X軸正方向楚昭,從這個點向下是Y軸正方向。
下面我們就來看看在上面兩種坐標系下位置的獲取拍顷、距離的獲取和View寬度的獲取 的方法抚太。
1中我們分析了Android屏幕的劃分,可以發(fā)現(xiàn)我們平時開發(fā)的重點其實都在關注View布局區(qū)域昔案,那么下面我們就來細說一下View區(qū)域常用的位置和距離尿贫。先看下面這幅圖:
通過上圖我們可以很直觀的給出View一些坐標相關的方法解釋,不過必須要明確的是上面這些方法必須要在layout之后才有效踏揣,如下:
View的靜態(tài)坐標方法 | 解釋 |
---|---|
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可見區(qū)域和頂點坐標的一些方法志珍,不過這些方法需要在Activity的onWindowFocusChanged ()方法之后才能使用橙垢。如下圖:
下面我們就給出上面這幅圖涉及的View的一些坐標方法的結(jié)果,如下所示:
View的方法 | 上圖View1結(jié)果 | 上圖View2結(jié)果 | 結(jié)論描述 |
---|---|---|---|
getLocalVisibleRect() | (0, 0, 410, 100) | (0, 0, 410, 470) | 獲取View自身可見的坐標區(qū)域伦糯,坐標以自己的左上角為原點(0,0)柜某,另一點為可見區(qū)域右下角相對自己(0,0)點的坐標嗽元,其實View2當前height為550,可見height為470喂击。 |
getGlobalVisibleRect() | (30, 100, 440, 200) | (30, 250, 440, 720) | 獲取View在屏幕絕對坐標系中的可視區(qū)域剂癌,坐標以屏幕左上角為原點(0,0),另一個點為可見區(qū)域右下角相對屏幕原點(0,0)點的坐標惭等。 |
getLocationOnScreen() | (30, 100) | (30, 250) | 坐標是相對整個屏幕而言珍手,Y坐標為View左上角到屏幕頂部的距離。 |
getLocationInWindow() | (30, 100) | (30, 250) | 如果為普通Activity則Y坐標為View左上角到屏幕頂部(此時Window與屏幕一樣大)辞做;如果為對話框式的Activity則Y坐標為當前Dialog模式Activity的標題欄頂部到View左上角的距離。 |
通過layout方法實現(xiàn)滑動
我們知道寡具,在View進行繪制時秤茅,會調(diào)用onLayout方法來設置顯示的位置。同樣童叠,可以通過修改View的mLeft, mTop, mRight, mBottom四個屬性來控制View的位置框喳。實現(xiàn)代碼如下所示:
@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實現(xiàn)滑動
這兩個方法相當于系統(tǒng)提供了一個對左右、上下移動的API的封裝厦坛。與上面一樣五垮,也是通過修改View的mLeft, mTop, mRight, mBottom四個屬性來控制View的位置,實現(xiàn)代碼如下:
@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實現(xiàn)滑動
LayoutParams保存了一個View的布局參數(shù)杜秸,因此可以在程序中放仗,通過改變LayoutParams來動態(tài)地修改一個View的布局參數(shù),從而達到改變View位置的效果撬碟。我們可以很方便的在程序中使用getLayoutParams()來獲取一個View的LayoutParams(注意必須在layout之后才可以獲取到)诞挨。實現(xiàn)代碼如下:
@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實現(xiàn)滑動
Google在其support庫中為我們提供了DrawerLayout和SlidingPaneLayout兩個布局來幫助開發(fā)者實現(xiàn)側(cè)邊欄滑動的效果。這兩個新的布局大大方便了我們創(chuàng)建自己的滑動布局界面呢蛤。然而惶傻,這兩個功能強大的布局背后隱藏著一個鮮為人知卻功能強大的類---ViewDragHelper。通過ViewDragHelper基本可以實現(xiàn)各種不同的滑動其障、拖放需求银室,因此此方法也是各種滑動解決方案中的終極絕招。
ViewDragHelper雖然功能強大励翼,但其使用方法也是最復雜的蜈敢。下面通過一個實例,來演示一下如何使用ViewDragHelper創(chuàng)建一個滑動布局抚笔,在這個例子中扶认,準備實現(xiàn)類似QQ滑動側(cè)邊欄的效果,初始時顯示內(nèi)容界面殊橙,當用戶手指滑動超過一定距離時辐宾,內(nèi)容界面?zhèn)然@示菜單界面狱从,整個過程下圖所示:
實現(xiàn)代碼如下所示:
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,第一個參數(shù)是要監(jiān)聽的View叠纹,通常需要是一個ViewGroup季研,
//即parentView;第二個參數(shù)是一個Callback回調(diào)誉察,后面會做解釋与涡。
mViewDragHelper = ViewDragHelper.create(this, callback);
}
//獲取菜單布局的寬度,之后可以根據(jù)菜單布局(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() {
// 何時開始檢測觸摸事件驼卖,通過這個方法,我們可以指定在創(chuàng)建ViewDragHelper時鸿秆,
//參數(shù)parentView中的哪一個View可以被移動酌畜。
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果當前觸摸的child是mMainView時開始檢測,并且只有mMainView可以被移動
return mMainView == child;
}
// 觸摸到View后回調(diào)
@Override
public void onViewCaptured(View capturedChild,
int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
// 當拖拽狀態(tài)改變卿叽,比如idle桥胞,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
// 當位置改變的時候調(diào)用,常用與滑動時更改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;
}
// 拖動結(jié)束后調(diào)用
@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內(nèi)部是利用Scroller實現(xiàn)滑動的,所以利用computeScroll方法實現(xiàn)平滑滑動
@Override
public void computeScroll() {
super.computeScroll();
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
通過scrollTo和scrollBy實現(xiàn)滑動
- 在一個View中考婴,系統(tǒng)提供了scrollTo贩虾、scrollBy兩種方式來改變一個View中初始可見內(nèi)容的位置。這兩個方法的區(qū)別非常好理解沥阱,與英文中To和By的區(qū)別類似缎罢,scrollTo(x, y)表示讓View中初始可見內(nèi)容的在水平方向偏移到點(- x, - y)(x大于零表示向左偏移,否者向右偏移; y大于零表示向上偏移喳钟,否者向右偏移)屁使,scrollBy(dx, dy)表示讓View中初始可見內(nèi)容的在水平方向偏移dx(dx大于零表示向左偏移,否者向右偏移)奔则,在垂直方向偏移dy(dy大于零表示向上偏移蛮寂,否者向右偏移)如下是這兩個方法的代碼實現(xiàn):
/**
* 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初始可見內(nèi)容的偏移量易茬。理解了mScrollX和mScrollY的用法酬蹋,就不難理解getScrollX() 和getScrollY()。這兩個函數(shù)的源碼如下所示:
/**
* 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;
}
-
舉例說明抽莱,如下圖所示(注意范抓,圖中黃色矩形區(qū)域表示的是View,綠色虛線矩形為View中初始可見的內(nèi)容食铐。一般情況下兩者的大小一致匕垫,本文為了顯示方便,將虛線框畫小了一點虐呻。圖中的黃色區(qū)域的位置始終不變象泵,發(fā)生偏移的是初始可見的內(nèi)容寞秃。):
scrollTo(0, 100)的效果如下圖所示:
scrollTo(100, 100)的效果圖如下:
若函數(shù)中參數(shù)為負值,則子View的移動方向?qū)⑾喾矗?/p>
通過Scroller實現(xiàn)滑動
上面舉例中通過scrollTo偏移View的初始可見內(nèi)容是在瞬間完成的偶惠,這樣的效果會讓人感覺非常突兀春寿。Google也想到了這一點,所以提供了Scroller類來模擬平滑滑動的效果忽孽。
Scroller類提供了startScroll方法來初始化一個模擬平滑滑動的過程绑改,然后調(diào)用invalidate()方法,這個方法會導致View重繪兄一,系統(tǒng)在繪制View的時候會在draw方法中調(diào)用computeScroll方法來實現(xiàn)模擬滑動厘线,在computeScroll方法中通過調(diào)用Scroller的computeScrollOffset方法判斷是否完成了整個滑動,同時Scroller也提供了getCurrX出革、getCurrY來獲取當前滑動過程中 View初始可見內(nèi)容 即將的偏移量皆的,然后利用srcollTo方法實現(xiàn)偏移即可,然后執(zhí)行invalidate方法實現(xiàn)循環(huán)調(diào)用computeScroll方法直到滑動結(jié)束蹋盆。
- 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初始可見內(nèi)容 開始滑動的位置(即mScrollX析藕,mScrollY的值),dx,dy分別為水平方向和垂直方向的偏移量(dx大于零表示向左偏移凳厢,否者向右偏移;dy大于零表示向上偏移账胧,否者向下偏移), duration為完成滾動的時間 。
mScroller.computeScrollOffset() //返回值為boolean先紫,true說明滑動尚未完成治泥,false說明滑動已經(jīng)完成。這是一個很重要的方法遮精,通常放在View.computeScroll()中居夹,用來判斷是否滑動是否結(jié)束。
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應用坐標系統(tǒng)全面詳解
- Android群英傳