国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統(tǒng) > Android > 正文

android全局監(jiān)控click事件的四種方式(小結(jié))

2019-12-12 02:16:27
字體:
供稿:網(wǎng)友

本文主要給大家分享如何在全局上去監(jiān)聽 click 點(diǎn)擊事件,并做些通用處理或是攔截。使用場景可能就是具體的全局防快速重復(fù)點(diǎn)擊,或是通用打點(diǎn)分析上報,用戶行為監(jiān)控等。以下將以四種不同的思路和實(shí)現(xiàn)方式去監(jiān)控全局的點(diǎn)擊操作,由簡單到復(fù)雜逐一講解。

方式一,適配監(jiān)聽接口,預(yù)留全局處理接口并作為所有監(jiān)聽器的基類使用

抽象出公共基類監(jiān)聽對象,可預(yù)留攔截機(jī)制和通用點(diǎ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);}

使用方式之一匿名對象作為公共監(jiān)聽器

CustClickListener mClickListener = new CustClickListener() {  @Override  protected void onViewClick(View view) {    Toast.makeText(CustActvity.this, view.toString(), Toast.LENGTH_SHORT).show();  }};@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {  super.onCreate(savedInstanceState);  setContentView(R.layout.activity_login);  findViewById(R.id.button).setOnClickListener(mClickListener);}

這種方式比較簡單,無兼容問題,但是需要自始至終都要使用基于基類的監(jiān)聽器對象,對開發(fā)者約束比較大。適用于新項目之初就有此使用約定。對于老代碼重構(gòu)工作量比較大,而且如果接入第三方墨盒模塊就無能為力了。

方式二,反射代理,適時偷梁換柱開發(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);      }    }  }}

我們需要選擇一個時機(jī)對所有設(shè)置有監(jiān)聽器的 View做監(jiān)聽代理的 hook .這個時機(jī)可以對 Activity 的根View添加一個視圖變化監(jiān)聽(當(dāng)然也可選擇在 Activity 的 DOWN 事件的分發(fā)時機(jī)):

rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {   @Override   public void onGlobalLayout() {    hookViews(rootView, 0)      }});

注:以上為了方便匿名注冊了監(jiān)聽,實(shí)際使用在 Activity 退出時要反注冊掉。

在進(jìn)行代理前先要反射獲取View監(jiān)聽器相關(guān)的 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成功獲取才能進(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)聽。只會對原來的 View 本身有點(diǎn)擊的事件監(jiān)聽器的進(jìn)行設(shè)置,成功設(shè)置后還會對操作的 View 設(shè)置一個 tag 標(biāo)志表明已經(jīng)設(shè)置了代理,避免每次變化重復(fù)設(shè)置。這個 tag 具有一定的含意,記錄該 View 相對可能存在的可回收容器的層級數(shù)。因?yàn)閷τ谙馎bsListView或RecyclerView的直接子 View 是需要強(qiáng)制重新綁定代理的,因?yàn)樗鼈兊膹?fù)用機(jī)制可能被重新設(shè)置了監(jiān)聽。

此方式實(shí)現(xiàn)實(shí)現(xiàn)稍微復(fù)雜,但是實(shí)現(xiàn)效果比較好,對開發(fā)者無感知進(jìn)行監(jiān)聽器的hook代理。反射效率上也可以接受速度比較快無影響。對任何設(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)時調(diào)用了 View.performClick 方法,內(nèi)部調(diào)用了sendAccessibilityEvent而此方法有個托管接口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;}

基于此原理我們可在某個時機(jī)給所有的 View 注冊我們自己的AccessibilityDelegate去監(jiān)聽系統(tǒng)行為事件,簡要實(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)測到window上全局視圖樹發(fā)生變化后遞歸的給所有的View安裝AccessibilityDelegate。經(jīng)測試大多數(shù)廠商的機(jī)型和版本都是可以的,然而部分機(jī)型無法成功捕獲監(jiān)控到點(diǎn)擊事件,所以不推薦使用。

方式四,通過分析 Activity 的 dispatchTouchEvent 事件并查找事件接受的目標(biāo) View。

這個方式初看有點(diǎn)匪夷所思,但是一系列觸屏事件發(fā)生后總歸要有一個組件消耗了它,查看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 都會被添加到mFirstTouchTarget這個鏈?zhǔn)綄ο罄铮益溄?jīng)過調(diào)整后 next 幾乎總是 null. 這就給我們一個突破口。可以從mFirstTouchTarget.child 得到當(dāng)前接受事件的直接子 View , 然后按此方法遞歸去查找直至mFirstTouchTarget.child 為 null。我們就算是找到了最終 touch 事件的接受者。這個查找最好的時機(jī)應(yīng)該是在ACTION_UP 或 ACTION_CANCEL 。

通過以上原理我們可以有法獲取一系列 Touch 事件最終接受處理的目標(biāo) View,再根據(jù)我們記錄的按下位置和松開位置及偏移偏量可判斷是否為可能的點(diǎn)擊動作。為了加強(qiáng)判斷是否為真正的 click 事件,可進(jìn)一步分析目標(biāo) View 是否安裝了點(diǎn)擊監(jiān)聽器(原理可參考上面講的方式二。以下獲取和分析事件時機(jī)都是在 Activity 的 dispatchTouchEvent 方法中進(jìn)行的。

記錄 down 和 up 事件后,以下為實(shí)現(xiàn)判斷是否為可能的點(diǎn)擊判斷

//whether it could be a click actionpublic 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 findprivate 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;}

通過以上方式所有具有點(diǎn)擊功能的 View 都能正確監(jiān)聽,然而可能存在并沒有監(jiān)聽點(diǎn)擊事件的 View 也被認(rèn)為是一次點(diǎn)擊事件。要過濾掉這部分可通過分析目標(biāo) View 是否安裝了點(diǎn)擊監(jiān)聽器,這里就不多貼代碼了,原理和代碼在方式二中有講過。

以上四種方式各有優(yōu)劣,效率上都比較快,綜合對比以方式二比較精準(zhǔn)。像方式三和試四只作為參考,具有學(xué)習(xí)意義,特別是方式四可應(yīng)用前景比較廣泛,所有的手勢的目標(biāo)View都可查找得到

本文講述的是我最近研究的用戶行為監(jiān)控的一個監(jiān)控點(diǎn)。具體更多的行為監(jiān)控請參考項目InteractionHook 目前還在持續(xù)開發(fā)中。

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持武林網(wǎng)。

發(fā)表評論 共有條評論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 姚安县| 宜良县| 易门县| 梅河口市| 宁德市| 八宿县| 新营市| 石景山区| 抚州市| 武穴市| 建水县| 辰溪县| 金寨县| 台中县| 抚松县| 贵州省| 棋牌| 缙云县| 麻城市| 旬邑县| 车致| 南宫市| 玉龙| 武冈市| 舞钢市| 武汉市| 新沂市| 伊川县| 元江| 博乐市| 纳雍县| 即墨市| 绵阳市| 晋中市| 吉水县| 榆林市| 叙永县| 凌云县| 英山县| 临猗县| 崇州市|