這是我重讀《Android 群英傳》的時候做的讀書筆記,這里主要講了 Android 坐標系和視圖坐標系,以及實現滑動的多種實現方法。
Android 坐標系和視圖坐標系
區別
Android 坐標系:左上角作為原點,由
getLocationScreen(int location[])
獲取點的位置,或在觸控事件中使用getRawX()
、getRawY()
獲得Android 坐標系中的坐標。視圖坐標系:子視圖在父視圖中的位置關系,同樣,父視圖的左上角為坐標原點,通過
getX()
、getY()
獲得視圖坐標系的坐標。
獲取坐標值
View 提供的獲取坐標方法
getTop()
、getLeft()
、getRight()
和 getBottiom()
獲取到的是 View 自身的頂邊、左邊、右邊、底邊到其父布局頂邊、左邊、右邊、底邊的距離。
MotionEvent 提供的方法
-
getX()
,getY()
獲取到點擊事件距離控件左邊、頂邊的距離,即視圖坐標 -
getRawX()
,getRawY()
獲取點擊事件距離整個屏幕左邊、頂邊的距離,即絕對坐標
實現滑動的方法
滑動的思想:觸摸 View 時,系統記下當前觸摸點坐標;當手指移動時,系統記下移動后的觸摸點坐標,從而獲取到相對于前一次坐標點的偏移量,并通過偏移量修改 View 的坐標。如此重復,從而實現滑動的過程。
1. layout 方法
@Override
public boolean onTouchEvent(MotionEvent event){
//絕對坐標,當然也可以通過 getX() 視圖坐標獲取偏移量, 兩種方式得到的偏移量都是相同的
//但是注意,使用絕對坐標系,一定要在每次執行完 ACTION_MOVE 的邏輯后,重設初始坐標,才能準確地獲取偏移量
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;
}
return true;
}
2. offsetLeftAndRight()
和 offsetTopAndBottom()
方法
與 layout 方法 效果相同,只是多了一層封裝而已
//同時對 left 和 right 進行偏移
offsetLeftAndRight(offsetX);
//同時對 top 和 bottom 進行偏移
offsetTopAndBottom(offsetY);
3. LayoutParams
LayoutParams 保存了一個 View 的布局參數,所以通過改變 LayoutParams 來動態修改一個布局的位置參數,從而達到改變 View 位置的效果。
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
所以,其實我們改變的是這個 View 的 Margin 屬性
4. scrollTo 和 scrollBy
scrollTo(x, y)
移動到一個具體的坐標點scrollBy(dx, dy)
移動的增量為 dx, dy
scrollTo 和 scrollBy 移動的是 View 的內容。即:對 TextView 使用的話,則是移動它的文本;對 ViewGroup 使用的話,則移動的是所有的子 View。所以,一般不對 View 使用這兩個方法,而是對 ViewGroup 使用。
int offsetX = x - lastX;
int offsetY = y - lastY;
//注:隨手指移動的話,偏移量要為負
((View)getParent()).scrollBy(-offsetX, -offsetY);
5. Scroller
Scroller 類可以實現平滑移動的效果,而不再是瞬間完成的移動。
Scroller 原理:和 scrollTo, scrollBy 類似,只是,在 ACTION_MOVE 中不斷獲取手指移動的微小偏移量,將一段距離劃分為 N 個非常小的偏移量。在每個偏移量里面通過 scrollBy 方法進行瞬間移動,實現平滑移動。
例:讓子 View 跟隨手指滑動,但在手指離開屏幕時,讓子 View 平滑移動到初始位置,即屏幕左上角。
使用 Scroller 類需要如下三個步驟:
a. 初始化 Scroller
mScroller = new Scroller(context);
b. 重寫 computeScroll()
方法,實現模擬滾動
//模板代碼
//系統在繪制 View 的時候會在 draw() 方法中調用該方法
//該方法實際上就是使用 scrollTo 方法, 不斷的瞬間移動一個小距離實現整體的平滑移動效果
@Override
public void computeScroll(){
super.computeScroll();
//判斷 Scroller 是否執行完畢
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(
mScroller.getCurrX(),//獲得當前的滑動坐標
mScroller.getCurrY());
//通過重繪不斷調用 computeScroll
invalidate();
}
}
需要注意:因為只能在 computeScroll()
方法中獲取模擬過程的 scrollX 和 scrollY 坐標,但 computeScroll()
不會自動調用,只能通過 invalidate()
-> draw()
-> computeScroll()
來間接調用 computeScroll()
,所以需要在上述代碼中調用invalidate()
,實現循環獲取 scrollX 和 scrollY 的目的。模擬過程結束后,scroller.computeScrollOfset()
方法會返回 false, 從而中斷循環,完成整個平滑移動的過程。
c. startScroll 開啟模擬過程
case MotionEvent.ACTION_UP:
//手指離開時,執行滑動過程,讓子 View 平滑移動到初始位置,即屏幕左上角
View viewGroup = ((View)getParent());
mScroller.startScroll(
viewGroup.getScrollX(),//起始坐標
viewGroup.getScrollY(),
-viewGroup.getScrollX(),//偏移量
-viewGroup.getScrollY());
//通知重繪!
invalidate();
break;
6. 屬性動畫
略(我也等筆記做到第7章的動畫機制的時候再寫吧,哈哈)
7. ViewDragHelper
例:實現 QQ 滑動側邊欄的布局
public class DragViewGroup extends FrameLayout {
private ViewDragHelper mViewDragHelper;
private View mMenuView, mMainView;
private int mWidth;
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();
}
//加載布局文件完成后調用
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//按順序將子 View 分別定義成 MenuView 和 MainView
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 獲取 View 的寬度
// 如果需要根據 View 的寬度來處理滑動后的效果,可以使用這個值來判斷
mWidth = mMenuView.getMeasuredWidth();
}
//步驟二:攔截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//將事件傳遞給 ViewDragHelper 處理
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸摸事件傳遞給ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
//步驟一:初始化
private void initView() {
//使用靜態工廠方法初始化
//參數1 通常是一個 ViewGroup,即parentView
//參數2 是一個 Callback 回調,是整個 ViewDragHelper 的邏輯核心
mViewDragHelper = ViewDragHelper.create(this, callback);
}
//步驟四:處理回調 Callback
private ViewDragHelper.Callback callback =
new ViewDragHelper.Callback() {
// 何時開始檢測觸摸事件
// 通過該方法,指定在創建 ViewDragHelper 時,參數 parentView 中的哪一個子 View 可以被移動
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果當前觸摸的child是mMainView時開始檢測
//即 只有 MainView 可以被拖動
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);
}
// 處理垂直滑動
// top: 垂直方向上 child 移動的距離
// dy: 相較前一次的增量
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;//垂直方向上不發生滑動
}
// 處理水平滑動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
// 拖動結束后調用
// 即手指離開屏幕后實現的操作
// 該方法內部是通過 Scroller 類來實現的
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//手指抬起后緩慢移動到指定位置
//讓 MainView 移動后左邊距小于500像素時,就使用 smoothSlideViewTo() 將 MainView 還原到初始狀態,即(0,0)的點
if (mMainView.getLeft() < 500) {
//關閉菜單
//相當于Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
} else {
//打開菜單
//大于500時,移動到(300, 0)坐標,即顯示 MenuView
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
//步驟三:處理 computeScroll()
//因為 ViewDragHelper 內部同樣是通過 Scroller 來實現平滑移動的,所以重寫該方法
//可作為模板代碼
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
ViewDragHelper.Callback 中其他的一些強大的事件
-
onViewCaptured()
觸摸到 View 后回調 -
onViewDragStateChanged()
拖拽狀態改變時回調 -
onViewPositionChanged()
位置改變時回調,如滑動時更改 scale 進行縮放等效果