您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關android中怎么全局監控click事件,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
方式一,適配監聽接口,預留全局處理接口并作為所有監聽器的基類使用
抽象出公共基類監聽對象,可預留攔截機制和通用點擊處理,簡要代碼如下:
public abstract class CustClickListener implements View.OnClickListener{ @Override public void onClick(View view) { if(!interceptViewClick(view)){ onViewClick(view); } } protected boolean interceptViewClick(View view){ //TODO:這里可做一此通用的處理如打點,或攔截等。 return false; } protected abstract void onViewClick(View view); }
使用方式之一匿名對象作為公共監聽器
CustClickListener mClickListener = new CustClickListener() { @Override protected void onViewClick(View view) { Toast.makeText(CustActvity.this, view.toString(), Toast.LENGTH_SHORT).show(); } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); findViewById(R.id.button).setOnClickListener(mClickListener); }
這種方式比較簡單,無兼容問題,但是需要自始至終都要使用基于基類的監聽器對象,對開發者約束比較大。適用于新項目之初就有此使用約定。對于老代碼重構工作量比較大,而且如果接入第三方墨盒模塊就無能為力了。
方式二,反射代理,適時偷梁換柱開發者無感知,在適配包裝器里做通用處理。
以下是代理接口和內置監聽適配器,全局的監聽接口需要實現IProxyClickListener并設置到內置適配器WrapClickListener里
public interface IProxyClickListener { boolean onProxyClick(WrapClickListener wrap, View v); class WrapClickListener implements View.OnClickListener { IProxyClickListener mProxyListener; View.OnClickListener mBaseListener; public WrapClickListener(View.OnClickListener l, IProxyClickListener proxyListener) { mBaseListener = l; mProxyListener = proxyListener; } @Override public void onClick(View v) { boolean handled = mProxyListener == null ? false : mProxyListener.onProxyClick(WrapClickListener.this, v); if (!handled && mBaseListener != null) { mBaseListener.onClick(v); } } } }
我們需要選擇一個時機對所有設置有監聽器的 View做監聽代理的 hook .這個時機可以對 Activity 的根View添加一個視圖變化監聽(當然也可選擇在 Activity 的 DOWN 事件的分發時機):
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { hookViews(rootView, 0) } });
注:以上為了方便匿名注冊了監聽,實際使用在 Activity 退出時要反注冊掉。
在進行代理前先要反射獲取View監聽器相關的 Method 和 Field 對象如下:
public void init() { if (sHookMethod == null) { try { Class viewClass = Class.forName("android.view.View"); if (viewClass != null) { sHookMethod = viewClass.getDeclaredMethod("getListenerInfo"); if (sHookMethod != null) { sHookMethod.setAccessible(true); } } } catch (Exception e) { reportError(e, "init"); } } if (sHookField == null) { try { Class listenerInfoClass = Class.forName("android.view.View$ListenerInfo"); if (listenerInfoClass != null) { sHookField = listenerInfoClass.getDeclaredField("mOnClickListener"); if (sHookField != null) { sHookField.setAccessible(true); } } } catch (Exception e) { reportError(e, "init"); } } }
只有保證了sHookMethod和sHookField成功獲取才能進入下一步遞歸去設置監聽代理偷梁換柱。以下為具體實現遞歸設置代理監聽的過程。其中mInnerClickProxy為外部傳入的的全局處理點擊事件的代理接口。
private void hookViews(View view, int recycledContainerDeep) { if (view.getVisibility() == View.VISIBLE) { boolean forceHook = recycledContainerDeep == 1; if (view instanceof ViewGroup) { boolean existAncestorRecycle = recycledContainerDeep > 0; ViewGroup p = (ViewGroup) view; if (!(p instanceof AbsListView || p instanceof RecyclerView) || existAncestorRecycle) { hookClickListener(view, recycledContainerDeep, forceHook); if (existAncestorRecycle) { recycledContainerDeep++; } } else { recycledContainerDeep = 1; } int childCount = p.getChildCount(); for (int i = 0; i < childCount; i++) { View child = p.getChildAt(i); hookViews(child, recycledContainerDeep); } } else { hookClickListener(view, recycledContainerDeep, forceHook); } } } private void hookClickListener(View view, int recycledContainerDeep, boolean forceHook) { boolean needHook = forceHook; if (!needHook) { needHook = view.isClickable(); if (needHook && recycledContainerDeep == 0) { needHook = view.getTag(mPrivateTagKey) == null; } } if (needHook) { try { Object getListenerInfo = sHookMethod.invoke(view); View.OnClickListener baseClickListener = getListenerInfo == null ? null : (View.OnClickListener) sHookField.get(getListenerInfo);//獲取已設置過的監聽器 if ((baseClickListener != null && !(baseClickListener instanceof IProxyClickListener.WrapClickListener))) { sHookField.set(getListenerInfo, new IProxyClickListener.WrapClickListener(baseClickListener, mInnerClickProxy)); view.setTag(mPrivateTagKey, recycledContainerDeep); } } catch (Exception e) { reportError(e,"hook"); } } }
以上深度優先從 Activity 的根 View 進行遞歸設置監聽。只會對原來的 View 本身有點擊的事件監聽器的進行設置,成功設置后還會對操作的 View 設置一個 tag 標志表明已經設置了代理,避免每次變化重復設置。這個 tag 具有一定的含意,記錄該 View 相對可能存在的可回收容器的層級數。因為對于像AbsListView或RecyclerView的直接子 View 是需要強制重新綁定代理的,因為它們的復用機制可能被重新設置了監聽。
此方式實現實現稍微復雜,但是實現效果比較好,對開發者無感知進行監聽器的hook代理。反射效率上也可以接受速度比較快無影響。對任何設置了監聽器的 View都有效。 然而AbsListView的Item點擊無效,因為它的點擊事件不是通過 onClick 實現的,除非不是用 setItemOnClick 而是自己綁定 click 事件。
方式三,通過AccessibilityDelegate捕獲點擊事件。
分析View的源碼在處理點擊事件的回調時調用了 View.performClick 方法,內部調用了sendAccessibilityEvent而此方法有個托管接口mAccessibilityDelegate可以由外部處理所有的 AccessibilityEvent. 正好此托管接口的設置也是開放的setAccessibilityDelegate,如以下 View 源碼關鍵片段。
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; } public void sendAccessibilityEvent(int eventType) { if (mAccessibilityDelegate != null) { mAccessibilityDelegate.sendAccessibilityEvent(this, eventType); } else { sendAccessibilityEventInternal(eventType); } } public void setAccessibilityDelegate(@Nullable AccessibilityDelegate delegate) { mAccessibilityDelegate = delegate; }
基于此原理我們可在某個時機給所有的 View 注冊我們自己的AccessibilityDelegate去監聽系統行為事件,簡要實現代碼如下。
public class ViewClickTracker extends View.AccessibilityDelegate { boolean mInstalled = false; WeakReference<View> mRootView = null; ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = null; public ViewClickTracker(View rootView) { if (rootView != null && rootView.getViewTreeObserver() != null) { mRootView = new WeakReference(rootView); mOnGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { View root = mRootView == null ? null : mRootView.get(); boolean install = ; if (root != null && root.getViewTreeObserver() != null && root.getViewTreeObserver().isAlive()) { try { installAccessibilityDelegate(root); if (!mInstalled) { mInstalled = true; } } catch (Exception e) { e.printStackTrace(); } } else { destroyInner(false); } } }; rootView.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); } } private void installAccessibilityDelegate(View view) { if (view != null) { view.setAccessibilityDelegate(ViewClickTracker.this); if (view instanceof ViewGroup) { ViewGroup parent = (ViewGroup) view; int count = parent.getChildCount(); for (int i = 0; i < count; i++) { View child = parent.getChildAt(i); if (child.getVisibility() != View.GONE) { installAccessibilityDelegate(child); } } } } } @Override public void sendAccessibilityEvent(View host, int eventType) { super.sendAccessibilityEvent(host, eventType); if (AccessibilityEvent.TYPE_VIEW_CLICKED == eventType && host != null) { //TODO 這里處理通用的點擊事件,host 即為相應被點擊的 View. } } }
以上實現比較巧妙,在監測到window上全局視圖樹發生變化后遞歸的給所有的View安裝AccessibilityDelegate。經測試大多數廠商的機型和版本都是可以的,然而部分機型無法成功捕獲監控到點擊事件,所以不推薦使用。
方式四,通過分析 Activity 的 dispatchTouchEvent 事件并查找事件接受的目標 View。
這個方式初看有點匪夷所思,但是一系列觸屏事件發生后總歸要有一個組件消耗了它,查看ViewGroup關鍵源碼如下:
// First touch target in the linked list of touch targets. private TouchTarget mFirstTouchTarget; public boolean dispatchTouchEvent(MotionEvent ev) { ...... if (newTouchTarget == null && childrenCount != 0) { for (int i = childrenCount - 1; i >= 0; i--) { if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } ...... // Dispatch to touch targets. 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 (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } }
這里發現意愿接受 touch 事件的 直接子View 都會被添加到mFirstTouchTarget這個鏈式對象里,且鏈經過調整后 next 幾乎總是 null. 這就給我們一個突破口。可以從mFirstTouchTarget.child 得到當前接受事件的直接子 View , 然后按此方法遞歸去查找直至mFirstTouchTarget.child 為 null。我們就算是找到了最終 touch 事件的接受者。這個查找最好的時機應該是在ACTION_UP 或 ACTION_CANCEL 。
通過以上原理我們可以有法獲取一系列 Touch 事件最終接受處理的目標 View,再根據我們記錄的按下位置和松開位置及偏移偏量可判斷是否為可能的點擊動作。為了加強判斷是否為真正的 click 事件,可進一步分析目標 View 是否安裝了點擊監聽器(原理可參考上面講的方式二。以下獲取和分析事件時機都是在 Activity 的 dispatchTouchEvent 方法中進行的。
記錄 down 和 up 事件后,以下為實現判斷是否為可能的點擊判斷
//whether it could be a click action public boolean isClickPossible(float slop) { if (mCancel || mDownId == -1 || mUpId == -1 || mDownTime == 0 || mUpTime == 0) { return false; } else { return Math.abs(mDownX - mUpX) < slop && Math.abs(mDownY - mUpY) < slop; } }
在 up 事件發生后立即查找目標 View.首先要保證反射 mFirstTouchTarge 相關的準備工作。
private boolean ensureTargetField() { if (sTouchTargetField == null) { try { Class viewClass = Class.forName("android.view.ViewGroup"); if (viewClass != null) { sTouchTargetField = viewClass.getDeclaredField("mFirstTouchTarget"); sTouchTargetField.setAccessible(true); } } catch (Exception e) { e.printStackTrace(); } try { if (sTouchTargetField != null) { sTouchTargetChildField = sTouchTargetField.getType().getDeclaredField("child"); sTouchTargetChildField.setAccessible(true); } } catch (Exception e) { e.printStackTrace(); } } return sTouchTargetField != null && sTouchTargetChildField != null; }
然后從 Activity 的 DecorView 去遞歸查找目標 View .
// find the target view who is interest in the touch event. null if not find private View findTargetView() { View nextTarget, target = null; if (ensureTargetField() && mRootView != null) { nextTarget = findTargetView(mRootView); do { target = nextTarget; nextTarget = null; if (target instanceof ViewGroup) { nextTarget = findTargetView((ViewGroup) target); } } while (nextTarget != null); } return target; } //reflect to find the TouchTarget child view,null if not found . private View findTargetView(ViewGroup parent) { try { Object target = sTouchTargetField.get(parent); if (target != null) { Object view = sTouchTargetChildField.get(target); if (view instanceof View) { return (View) view; } } } catch (Exception e) { e.printStackTrace(); } return null; }
以上就是android中怎么全局監控click事件,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。