本篇文章給大家分享的是有關(guān)android中怎么全局監(jiān)控click事件,小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

目前成都創(chuàng)新互聯(lián)已為1000+的企業(yè)提供了網(wǎng)站建設(shè)、域名、網(wǎng)頁空間、網(wǎng)站托管運(yùn)營(yíng)、企業(yè)網(wǎng)站設(shè)計(jì)、赤壁網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長(zhǎng),共同發(fā)展。
方式一,適配監(jiān)聽接口,預(yù)留全局處理接口并作為所有監(jiān)聽器的基類使用
抽象出公共基類監(jiān)聽對(duì)象,可預(yù)留攔截機(jī)制和通用點(diǎn)擊處理,簡(jiǎn)要代碼如下:
public abstract class CustClickListener implements View.OnClickListener{
@Override
public void onClick(View view) {
if(!interceptViewClick(view)){
onViewClick(view);
}
}
protected boolean interceptViewClick(View view){
//TODO:這里可做一此通用的處理如打點(diǎn),或攔截等。
return false;
}
protected abstract void onViewClick(View view);
}使用方式之一匿名對(duì)象作為公共監(jiān)聽器
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);
}這種方式比較簡(jiǎn)單,無兼容問題,但是需要自始至終都要使用基于基類的監(jiān)聽器對(duì)象,對(duì)開發(fā)者約束比較大。適用于新項(xiàng)目之初就有此使用約定。對(duì)于老代碼重構(gòu)工作量比較大,而且如果接入第三方墨盒模塊就無能為力了。
方式二,反射代理,適時(shí)偷梁換柱開發(fā)者無感知,在適配包裝器里做通用處理。
以下是代理接口和內(nèi)置監(jiān)聽適配器,全局的監(jiān)聽接口需要實(shí)現(xiàn)IProxyClickListener并設(shè)置到內(nèi)置適配器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);
}
}
}
}我們需要選擇一個(gè)時(shí)機(jī)對(duì)所有設(shè)置有監(jiān)聽器的 View做監(jiān)聽代理的 hook .這個(gè)時(shí)機(jī)可以對(duì) Activity 的根View添加一個(gè)視圖變化監(jiān)聽(當(dāng)然也可選擇在 Activity 的 DOWN 事件的分發(fā)時(shí)機(jī)):
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
hookViews(rootView, 0)
}
});注:以上為了方便匿名注冊(cè)了監(jiān)聽,實(shí)際使用在 Activity 退出時(shí)要反注冊(cè)掉。
在進(jìn)行代理前先要反射獲取View監(jiān)聽器相關(guān)的 Method 和 Field 對(duì)象如下:
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成功獲取才能進(jìn)入下一步遞歸去設(shè)置監(jiān)聽代理偷梁換柱。以下為具體實(shí)現(xiàn)遞歸設(shè)置代理監(jiān)聽的過程。其中mInnerClickProxy為外部傳入的的全局處理點(diǎn)擊事件的代理接口。
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);//獲取已設(shè)置過的監(jiān)聽器
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");
}
}
}以上深度優(yōu)先從 Activity 的根 View 進(jìn)行遞歸設(shè)置監(jiān)聽。只會(huì)對(duì)原來的 View 本身有點(diǎn)擊的事件監(jiān)聽器的進(jìn)行設(shè)置,成功設(shè)置后還會(huì)對(duì)操作的 View 設(shè)置一個(gè) tag 標(biāo)志表明已經(jīng)設(shè)置了代理,避免每次變化重復(fù)設(shè)置。這個(gè) tag 具有一定的含意,記錄該 View 相對(duì)可能存在的可回收容器的層級(jí)數(shù)。因?yàn)閷?duì)于像AbsListView或RecyclerView的直接子 View 是需要強(qiáng)制重新綁定代理的,因?yàn)樗鼈兊膹?fù)用機(jī)制可能被重新設(shè)置了監(jiān)聽。
此方式實(shí)現(xiàn)實(shí)現(xiàn)稍微復(fù)雜,但是實(shí)現(xiàn)效果比較好,對(duì)開發(fā)者無感知進(jìn)行監(jiān)聽器的hook代理。反射效率上也可以接受速度比較快無影響。對(duì)任何設(shè)置了監(jiān)聽器的 View都有效。 然而AbsListView的Item點(diǎn)擊無效,因?yàn)樗狞c(diǎn)擊事件不是通過 onClick 實(shí)現(xiàn)的,除非不是用 setItemOnClick 而是自己綁定 click 事件。
方式三,通過AccessibilityDelegate捕獲點(diǎn)擊事件。
分析View的源碼在處理點(diǎn)擊事件的回調(diào)時(shí)調(diào)用了 View.performClick 方法,內(nèi)部調(diào)用了sendAccessibilityEvent而此方法有個(gè)托管接口mAccessibilityDelegate可以由外部處理所有的 AccessibilityEvent. 正好此托管接口的設(shè)置也是開放的setAccessibilityDelegate,如以下 View 源碼關(guān)鍵片段。
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;
}基于此原理我們可在某個(gè)時(shí)機(jī)給所有的 View 注冊(cè)我們自己的AccessibilityDelegate去監(jiān)聽系統(tǒng)行為事件,簡(jiǎn)要實(shí)現(xiàn)代碼如下。
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 這里處理通用的點(diǎn)擊事件,host 即為相應(yīng)被點(diǎn)擊的 View.
}
}
}以上實(shí)現(xiàn)比較巧妙,在監(jiān)測(cè)到window上全局視圖樹發(fā)生變化后遞歸的給所有的View安裝AccessibilityDelegate。經(jīng)測(cè)試大多數(shù)廠商的機(jī)型和版本都是可以的,然而部分機(jī)型無法成功捕獲監(jiān)控到點(diǎn)擊事件,所以不推薦使用。
方式四,通過分析 Activity 的 dispatchTouchEvent 事件并查找事件接受的目標(biāo) View。
這個(gè)方式初看有點(diǎn)匪夷所思,但是一系列觸屏事件發(fā)生后總歸要有一個(gè)組件消耗了它,查看ViewGroup關(guān)鍵源碼如下:
// 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;
}
}
}這里發(fā)現(xiàn)意愿接受 touch 事件的 直接子View 都會(huì)被添加到mFirstTouchTarget這個(gè)鏈?zhǔn)綄?duì)象里,且鏈經(jīng)過調(diào)整后 next 幾乎總是 null. 這就給我們一個(gè)突破口。可以從mFirstTouchTarget.child 得到當(dāng)前接受事件的直接子 View , 然后按此方法遞歸去查找直至mFirstTouchTarget.child 為 null。我們就算是找到了最終 touch 事件的接受者。這個(gè)查找最好的時(shí)機(jī)應(yīng)該是在ACTION_UP 或 ACTION_CANCEL 。
通過以上原理我們可以有法獲取一系列 Touch 事件最終接受處理的目標(biāo) View,再根據(jù)我們記錄的按下位置和松開位置及偏移偏量可判斷是否為可能的點(diǎn)擊動(dòng)作。為了加強(qiáng)判斷是否為真正的 click 事件,可進(jìn)一步分析目標(biāo) View 是否安裝了點(diǎn)擊監(jiān)聽器(原理可參考上面講的方式二。以下獲取和分析事件時(shí)機(jī)都是在 Activity 的 dispatchTouchEvent 方法中進(jìn)行的。
記錄 down 和 up 事件后,以下為實(shí)現(xiàn)判斷是否為可能的點(diǎn)擊判斷
//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 事件發(fā)生后立即查找目標(biāo) View.首先要保證反射 mFirstTouchTarge 相關(guān)的準(zhǔn)備工作。
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 去遞歸查找目標(biāo) 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中怎么全局監(jiān)控click事件,小編相信有部分知識(shí)點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見到或用到的。希望你能通過這篇文章學(xué)到更多知識(shí)。更多詳情敬請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。
分享名稱:android中怎么全局監(jiān)控click事件
文章鏈接:http://www.chinadenli.net/article16/ispidg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供手機(jī)網(wǎng)站建設(shè)、企業(yè)網(wǎng)站制作、全網(wǎng)營(yíng)銷推廣、定制網(wǎng)站、搜索引擎優(yōu)化、網(wǎng)站內(nèi)鏈
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)