溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

android中怎么全局監(jiān)控click事件

發(fā)布時間:2021-06-26 16:56:35 來源:億速云 閱讀:636 作者:Leah 欄目:移動開發(fā)

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

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

抽象出公共基類監(jiān)聽對象,可預(yù)留攔截機制和通用點擊處理,簡要代碼如下:

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);
}

使用方式之一匿名對象作為公共監(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)聽器對象,對開發(fā)者約束比較大。適用于新項目之初就有此使用約定。對于老代碼重構(gòu)工作量比較大,而且如果接入第三方墨盒模塊就無能為力了。

方式二,反射代理,適時偷梁換柱開發(fā)者無感知,在適配包裝器里做通用處理。

以下是代理接口和內(nèi)置監(jiān)聽適配器,全局的監(jiān)聽接口需要實現(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);
      }
    }
  }
}

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

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

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

在進行代理前先要反射獲取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成功獲取才能進入下一步遞歸去設(shè)置監(jiān)聽代理偷梁換柱。以下為具體實現(xiàn)遞歸設(shè)置代理監(jiān)聽的過程。其中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);//獲取已設(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 進行遞歸設(shè)置監(jiān)聽。只會對原來的 View 本身有點擊的事件監(jiān)聽器的進行設(shè)置,成功設(shè)置后還會對操作的 View 設(shè)置一個 tag 標(biāo)志表明已經(jīng)設(shè)置了代理,避免每次變化重復(fù)設(shè)置。這個 tag 具有一定的含意,記錄該 View 相對可能存在的可回收容器的層級數(shù)。因為對于像AbsListView或RecyclerView的直接子 View 是需要強制重新綁定代理的,因為它們的復(fù)用機制可能被重新設(shè)置了監(jiān)聽。

此方式實現(xiàn)實現(xiàn)稍微復(fù)雜,但是實現(xiàn)效果比較好,對開發(fā)者無感知進行監(jiān)聽器的hook代理。反射效率上也可以接受速度比較快無影響。對任何設(shè)置了監(jiān)聽器的 View都有效。 然而AbsListView的Item點擊無效,因為它的點擊事件不是通過 onClick 實現(xiàn)的,除非不是用 setItemOnClick 而是自己綁定 click 事件。

方式三,通過AccessibilityDelegate捕獲點擊事件。

分析View的源碼在處理點擊事件的回調(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;
}

基于此原理我們可在某個時機給所有的 View 注冊我們自己的AccessibilityDelegate去監(jiān)聽系統(tǒng)行為事件,簡要實現(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 這里處理通用的點擊事件,host 即為相應(yīng)被點擊的 View.
    }
  }
}

以上實現(xiàn)比較巧妙,在監(jiān)測到window上全局視圖樹發(fā)生變化后遞歸的給所有的View安裝AccessibilityDelegate。經(jīng)測試大多數(shù)廠商的機型和版本都是可以的,然而部分機型無法成功捕獲監(jiān)控到點擊事件,所以不推薦使用。

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

這個方式初看有點匪夷所思,但是一系列觸屏事件發(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 事件的接受者。這個查找最好的時機應(yīng)該是在ACTION_UP 或 ACTION_CANCEL 。

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

記錄 down 和 up 事件后,以下為實現(xià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事件,小編相信有部分知識點可能是我們?nèi)粘9ぷ鲿姷交蛴玫降?。希望你能通過這篇文章學(xué)到更多知識。更多詳情敬請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI