編輯:關於android開發
View 是 Android 中所有空間的基類。
View 的位置主要有四個頂點決定的, top、left、right、bottom. 這些頂點的坐標是相對於 View 的父容器來說。
從 Android 3.0 增加的參數: x、 y、 translationX 和 translationY. translationX 和 translationY 是 View 左上角相對於父容器的偏移量。
x = left + translationX, y = top + translationY;
在 View 平移的過程中, top 和 left 表示的是原始左上角的位置信息, 其值並不會發生變化, 改變的是 x, y, traslationX 和 translation Y.
3、 MotionEvent 和 TouchSlop
MotionEvent分為 ACTION_DOWN, ACTION_MOVE, ACTION_UP
獲取點擊事件的坐標
getX/getY 獲取當前 View 左上角的的 x, y 坐標;
getRawX/getRawY 返回的是相對於手機屏幕左上角的 x 和 y 的坐標(即手指在屏幕中的坐標);
ToushSlop
系統所能識別出的被認為是滑動的最小距離。 常量, 和設備有關,不同的設備上可以有所不同。
通過 get(getContext()).getScaledTouchSlop() 獲取。
4、VelocityTracker, GestureDetector 和 Scroller
VelocityTracker
速度追蹤,用於追蹤手指在滑動過程中的速度,包括水平和豎直方向的速度。在 Support Library 中是VelocityTrackerCompat.
官網的連接 , 使用如下:
// 1、在 View 的 onTouchEvent 方法中追蹤當前點擊事件的速度 VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); // 2、獲取當前的速度, 單位以 1000 ms 為例 velocityTracker.computeCurrentVelocity(1000); int xVelocity = (int) velocityTracker.getXVelocity(); int yVelocity = (int) velocityTracker.getYVelocity(); // 3、最後進行回收 velocityTracker.recycle();
GestureDetector
手勢檢測,用於輔助檢測用戶的單擊、滑動、長按、雙擊等行為。
Gathering data about touch events;
Interpreting the data to see if it meets the criteria for any of gestures your app supports.
// 1、創建一個 GestureDetector 對象,並實現 OnGestureListener 接口。 GestureDetector gestureDetector = new GestureDetector(AnimationActivity.this, this); // 2、接管目標 View 的 onTouchEvent 方法, 在待監聽 View 的 onTouchEvent 方法中添加實現 boolean consume = gestureDetector.onTouchEvent(event); return; consume;
OnGestureListener 接口的方法
onDrown : 手指輕輕觸摸屏幕的瞬間,由一個 ACTION_DOWN 觸發;
onShowPress : 手指輕觸屏幕,尚未松開或者拖動, 由一個 ACTION_DOWN 觸發;
onSingTapUp : 手指松開,由 ACTION_DOWN 觸發, 這是單擊行為;
onScroll: 手指按下屏幕並拖動,由一個 ACTION_DOWN , 多個 ACTION_MOVE 觸發, 這是拖動行為;
onLongPress: 用戶長久地按著屏幕不放,即長按;
onFling: 用戶按下觸摸屏,快速移動後松開,由一個 ACTION_DWON、多個 ACTION_MOVE 和一個 ACTION_UP 觸發,快速滑動行為;
OnDoubleTapListener 接口中的方法
onDoubleTap: 雙擊,由兩次連續的單擊組成,不能和 onSingleTapConfirmed 共存;
onSingleTapConfirmed: 單擊行為;
onDoubleEvent: 表示雙擊行為,在雙擊的期間, ACTION_DOWN、ACTION_MOVE、ACTION_UP 都不會觸發此回調。
如果只是監聽滑動相關的,可以在 onTouchEvent 方法中實現,如果要監聽類似雙擊這樣的行為,使用 GestureDetector.
Where or not you use GestureDetector.OnGesturelistener, it's best practive to implement onDraw() method that reture true. This is beacause all getstures begin with an OnDraw() message, if you reture false for onDraw(), the system assumes methods of GestureDetector.OnGestureListener never get called.
5.Scroller
用於實現 View 的彈性滑動
通過 View 本身提供的 srollTo/srollBy 方法來實現滑動;
通過動畫給 View 施加平移效果來實現滑動;
通過改變 View 的 LayoutParams 使得 View 重新布局從而實現滑動。
1、使用 ScrollTo/ScrollBy
滑動過程是通過改變 View 的 mScrollX 和 mScrollY 的值,實現滑動。在滑動過程中, mScrollX 的值總是等於 View 左邊緣和 View 內容左邊緣在水平的距離。
ScrollTo/ScrollBy 只能改變 View 內容的位置,而不能改變 View 在布局中的位置。
2、使用動畫
使用動畫來移動 View, 主要是操作 View 的translationX 和 translationY 屬性。
如果使用 View 動畫滑動時, View 動畫是對 View 的影響操作,它 並不能改變 View 的位置參數,包括寬高,如果希望動畫後的狀態得以保留,設置 fillAfter 為 true, 否則動畫完成後其動畫結果就會消失。
使用屬性動畫則無問題。
3、改變布局參數
即通過改變 LayoutParams.
例子:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams(); params.width +=100; params.leftMargin += 100; mButton.requestLayout(); // 或者 mButton.setLayoutParams(params);
.scrollTo/scrollBy : 操作簡單, 適合對 View 內容的滑動;
.動畫:操作簡單, 主要適用於沒有交互的 View 和實現復雜的動畫效果;
.改變布局參數:操作稍微復雜,適用於有交互的 View.
實現彈性滑動的共同點,即將一次大的滑動分成若干次小的滑動,並在一個時間段內完成。
Scroller mScroller = new Scroller(mContext); // 緩慢滾動到指定的位置 private void smoothScrollTo(int destX, int destY){ int scrollX = getScrollX(); int deltaX = destX - scrollX; // 以 1000ms 內滑向 destX, 效果是慢慢滑動 mScroller.startScroll(scrollX, destY, deltaX , 0, 1000); // View 的重繪 invalidate(); } @Override public void computeScroll() { // 重寫 computeScroll 方法,並在內部完成平滑滾動的邏輯 if (mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 再次進行重繪 postInvalidate(); } }
使用 Scroller 內部的整個流程
Scroller 的工作原理:
Scroller 本身不能實現 View 的滑動,它需要配合 View 的 computeScroll 方法才能完成彈性滑動的效果。通過不斷地讓 View 重繪,而每一次重繪距離滑動其實起始時間會有一個時間間隔,通過這個時間間隔 Scroller 得出 View 當前的滑動位置,知道了滑動位置就可以通過 scrollTo 方法完成 View 的滑動。 View 的每一次重繪都會導致 View 的小幅度滑動,而多次的小幅度滑動組成了彈性滑動,這就是 Scroller 滑動的工作機制。
Scroller 的使用步驟:
1、創建 Scroller 實例;
2、調用 startScroll(...) 方法來初始化滑動數據並刷新界面;
3、重寫 computeScroll() 方法, 並在其內部完成滑動的邏輯。
可參考郭霖的博客http://blog.csdn.net/guolin_blog/article/details/48719871;
通過屬性動畫
ObjectAnimatior.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
通過 ValueAnimator,這裡只是改變 Button 的內容
private void scroller(){ final int startX = 0; final int deltax = 1000; final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(3000); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = animator.getAnimatedFraction(); mButton.scrollTo(startX +(int)(deltax * fraction), 0); } }); animator.start(); }3、使用延時策略
通過發送一系列的消息從而達到一種漸進式的效果,具體來說就是使用 Handler 或 View 的 postDelayed 方法,也可以使用線程的 sleep 方法。
所謂點擊事件的分發,其實就是對 MotionEvent 事件分發的過程,即當一個 MotionEvent 產生後,系統需要把這個事件傳遞給一個具體的 View, 而這個傳遞的過程就是分發過程。
所有 Touch 事件都被封裝成了 MotionEvent 對象,包括 Touch 的位置、時間、歷史記錄以及幾個手指(多指觸摸)等。
每個事件是以 ACTION_DOWN 開始,ACTION_UP 結束。
對事件的處理包括三類:
傳遞:dispatchTouchEvent(...);
攔截:onInterceptTouchEvent(...);
消費:onTouchEvent 和 OnTouchListener;
它們之間的關系
public boolean dispatchTouchEvent(MotionEvent event){ boolean consume = false; if (onInterceptTouchEvent(envetn)){ consume = onTouchEvent(event); } else { consume = child.dispatchTouchEvent(event); } return consume; } }
事件中的優先級 onTouchListener > onTouchEvent > onClickListener;
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { // 空方法,不執行任何東西 onUserInteraction(); } // 調用 Window.superDispatchTouchEvent(...), Window 的實現類是 PhoneWindow // 所以,對調用 PhoneWindow.superDispatchTouchEvent(...) if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }下面是Activity 傳遞點擊事件的流程
PhoneWindow 為 Window 的實現類, 在 Activity#attch(...) 方法中實例
// Actvity # attach(...)方法 final void attach(...) { ... mWindow = new PhoneWindow(this); ... }PhoneWindow#superDispatchTouchEvent(...) 方法
// PhoneWindow#superDispatchTouchEvent(..) // 在 PhoneWindow 中 mDecor 為 DecorView, DecorView 繼承 FrameLayout public boolean superDispatchTouchEvent(MotionEvent event) { // 將點擊事件傳遞給 DecorView return mDecor.superDispatchTouchEvent(event); }
如果Activity 中的 View 不攔截或者不消耗點擊事件, 則會執行 Activity 的 onTouchEvent(...) 方法。
// Activity#onTouchEvent(...) public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
Activity#onTouchEvent(...) 方法會調用 Window#shouldCloseOnTouche(...) 方法,如果該方法返回 true,則結束當前 Activity ,並讓 Activity#onTouchEvent(...) 返回 true,表示當前的 Activity 消耗點擊事件;否則讓Activity#onTouchEvent(...) 返回 false, 表示當前Activity 不消耗點擊事件。
在看看 Window#shouldCloseOnTouch(...)源碼
// Window#shouldCloseOnTouch(...) public boolean shouldCloseOnTouch(Context context, MotionEvent event) { if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) && peekDecorView() != null) { return true; } return false; }
說明:
.mCloseOnTouchOutside 是一個 boolean 變量,它是由 Window 的 android:windowCloseOnOutside 屬性覺得的。可以通過 Activity#setFinishOnTouchOutside(...) 方法設定,Activity#setFinishOnTouchOutside(...)方法會調用 Window#setCloseOnTouchOutside(...) 方法;
.isOutofBounds(context, event) 判斷 event 的坐標是否在 context(對於當前來說是 Activity)之外;
.peekDecorView() 返回 PhoneWindow 的 mDecor;
mCloseOnTouchOutside 這個變量非常有用,特別是 Activity 使用 Dialog 的 style 時,如果想點擊彈窗外部不結束 Activity ,可以設置 Activity#setFinishOnTouchOutsidee(false) 。因為該方法會通過 Window#setCloeOnTouchOutside(...) 方法,使 mCloseOnTouchOutside 為 false , 最終Activity 不消耗該點擊事件。
ViewGroup#dispatchTouchEvent(...)
// VieGroup#dispatchTouchEvent(...) // 1. action 為 ACTION_DOWN 或者 mFirstTouchTarget 不為空是,執行 if 裡面的內容,否則執行 // else 裡面的內容,intercept = true // 2. mFirstTouchTarget 是接收點擊事件的 View 組成的單鏈表 // Check for interception. final boolean intercepted; if(actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null){ // FLAG_DISALLOW_INTERCEPT 標記位,通過調用 requestDisallowInterceptTouchEvent() 設置, // 1.設置之後,FLAG_DISALLOW_INTERCEPT 為 true, 禁止其父類對點擊事件進行攔截, 可參考 ViewPager // 2.設置之後,ViewGroup 將無法攔截 ACTION_DOWN 以外的點擊事件。 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 如果 FLAG_DISALLOW_INTERCEPT 為 false, 調用 onInterceptTouchEvent(ev) 方法,並返回是否攔截; // onInterceptTouchEvent(...) 方法為 false,如果想攔截,則重寫該方法,讓它返回 true; intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } }else{ // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
如果沒有被取消並且沒有被攔截,則對將點擊事件分發給子 View
// ViewGroup#dispatchTouchEvent(...) // 沒有被取消並且非攔截的狀態下 if(!canceled&&!intercepted){ 。。。 // 遍歷所有的子 View,並對點擊事件進行分發 final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); 。。。 // 1.canViewReceivePointerEvents(...) 判斷子 View 能否接收到點擊事件 // 判斷的條件是 child 可見 或者 不可見但出於動畫狀態 // 2. isTransformedTouchPointInView(...) 判斷點擊坐標(x,y) 是否在 child 可視范圍之內 // 如果當前的 child 能接收點擊事件並且點擊的坐標在 child 的可視范圍只能,點擊事件交給當前的 child 處理, // 否則執行 continue。 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); // 調用 dispatchTransformedTouchEvent(...) 將點擊事件分發給子 View, if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); // 如果 child 接受點擊事件,即 child 對點擊事件進行了消耗或者攔截 // 調用 addTouchTarget(...) 將 child 添加到 mFirstTouchTarget 鏈表的表頭,並返回鏈表 TouchTarget // 將 alreadyDispatchedToNewTouchTarget 設置為true newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } // Dispatch to touch targets. // 進一步分發點擊事件 // (01) 如果mFirstTouchTarget為null,意味著還沒有任何View來接受該觸摸事件; // 此時,將當前ViewGroup看作一個View; // 將會調用"當前的ViewGroup的父類View的dispatchTouchEvent()"對觸摸事件進行分發處理。 // 即,會將觸摸事件交給當前ViewGroup的onTouch(), onTouchEvent()進行處理。 // (02) 如果mFirstTouchTarget不為null,意味著有ViewGroup的子View或子ViewGroup中, // 有可以接受觸摸事件的。那麼,就將觸摸事件分發給這些可以接受觸摸事件的子View或子ViewGroup。 if(mFirstTouchTarget==null){ // No touch targets so treat this as an ordinary view. // 第三年參數為空,則意味中交給當前 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }else{ // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } }說明:dispatchTransformedTouchEvent(...)對點擊事件從新打包分發
如果第三個參數為 null 時,則會調用 super.dispatchTouchEvent(...), 而ViewGroup 是繼承 View 的,這時就將點擊事件傳到 View#dispatchTouchEvent 方法;如果不為空,則會調用 child.dispatchTouchEvent(...), 將點擊事件分發給子 View。
View#dispatchTouchEvent(...) 源碼
// View#dispatchTouchEvent(...) public boolean dispatchTouchEvent(MotionEvent event) { ... boolean result = false; ... if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; // 1、是否設置了 onTouchListener // 2. 是否設置 ENABLED, 在 xml 中是 android: enabled 或者 在 java 代碼中 設置 View#setEnabled // 3. OnTouchLisetener.onTouch(...) 方法返回 // 如果mOnTouchListener.onTouch(...) 方法返回 true, 則不會執行後面的 onTouchEvent(...) 方法,從這裡可以看出 // OnTouch(...)方法比 onTouchEvent(...)高級。 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 執行 onTouchEvent(...) 方法 if (!result && onTouchEvent(event)) { result = true; } } ... return result; }View#onTouchEvent(...) 的源碼
// View#onTouchEvent(...) public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); // 即使 View 被禁用了, 也會消耗點擊事件 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } // 是否設置了代理 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 點擊狀態時 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ... if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { // 調用 performClick() 方法,裡面會調用 playSoundEffect(...) 方法和 // OnClickListener.onClick(...) 方法 performClick(); } } } .... } break; ... } return true; } return false; }在 View#onTouchEvent(...) 中的 ACTION_UP中會調用 performClick(...) 方法,並在裡面調用 OnClickListener.onClick(...) 方法。
關於在源碼中分析點擊傳遞過程,可參考下面的的博文,本文有部分也是從下面博文中摘取的。
《Android 觸摸事件機制(二)Activity 中觸摸事件詳解》
《Android 觸摸事件機制(三)View 中觸摸事件詳解》
《Android 觸摸事件機制(四)ViewGroup 中觸摸事件詳解》
public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; // 獲取點擊的坐標 int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (父容器需要當前點擊事件){ intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }說明: . 在 onInterceptTouchEvent 方法中, ACTION_DOWN 這個事件,父容器必須返回 false, 即不攔截 ACTION_DOWN 事件, 這是因為一旦父容器攔截了 ACTION_DOWN, 那麼後續的 ACTION_MOVE 和 ACTION_UP 事件都會直接交由父容器處理,這個時候事件沒法再傳遞給子元素了; . 其次是 ACTION_MOVE 事件,這個事件可以根據需要決定是否攔截,如果父容器需要攔截就返回 true, 否則返回 false; . 最後是 ACTION_UP 事件,必須返回 false.
內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事就直接消耗,否則就交由父容器進行處理。這種方法和 Android 中的事件分發不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作。
重寫子 View 的 dispatchTouchEvent(...) 方法
public boolean dispatchTouchEvent(MotionEvent ev) { int x = ev.getX(); int y = ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: parent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要此類點擊事件){ parent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } return super.dispatchTouchEvent(ev); }父容器也要做相應的改動
public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN){ return false; } else { return true; } }
Android熱補丁動態修復實踐 前言 好幾個月之前關於Android App熱補丁修復火了一把,源於QQ空間團隊的一篇文章安卓App熱補丁動態修復技術介紹,然後各大
淺談android:clipChildren屬性,實現功能: 1、APP主界面底部模塊欄 2、ViewPager一屏多個界面顯示 3、........ 首先
Kotlin的擴展函數:擴展Android框架(KAD 08),kotlinandroid作者:Antonio Leiva 時間:Jan 11, 2017 原文鏈接:ht
我的android學習經歷12,android學習經歷12自動匹配輸入的內容(文章最後有一個問題有興趣的可以解答一下,謝謝大神了) 這個主要是兩個控件MultiAutoC