android全局监控click事件的四种方式(小结)

本文主要给大家分享如何在全局上去监听 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;
}

通过以上方式所有具有点击功能的 View 都能正确监听,然而可能存在并没有监听点击事件的 View 也被认为是一次点击事件。要过滤掉这部分可通过分析目标 View 是否安装了点击监听器,这里就不多贴代码了,原理和代码在方式二中有讲过。

以上四种方式各有优劣,效率上都比较快,综合对比以方式二比较精准。像方式三和试四只作为参考,具有学习意义,特别是方式四可应用前景比较广泛,所有的手势的目标View都可查找得到

本文讲述的是我最近研究的用户行为监控的一个监控点。具体更多的行为监控请参考项目InteractionHook目前还在持续开发中。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android中button实现onclicklistener事件的两种方式

    复制代码 代码如下: package com.demos; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class GetScreenActivity extends Activity { private Button fi

  • 三种Android单击事件onclick的实现方法

    onclick事件的定义方法,分为三种,分别为在xml中进行指定方法:在Actitivy中new出一个OnClickListenner():实现OnClickListener接口三种方式. 代码分别如下: 1. xml指定onclick事件,这种方式比较适用于指定的button,能使java代码相对简化一些: xml文件中: <span style="color:#333333;"><Button android:text="Button03" a

  • 详谈Android中onTouch与onClick事件的关系(必看)

    这几天遇到点关于Android的触摸事件相关的,还跟onClick有关,暂且记下: LinearLayout分别设置了onTouchListener,onClickListener,onLongClickListener及onTouchEvent回调 1.在屏幕上触摸之后基本的执行流程如下: onTouch,action=0 onTouchEvent,action=0 onTouch,action=2 onTouchEvent,action=2 onTouch,action=2 onTouchE

  • Android ButtonOnClick事件的写法总结

    Android ButtonOnClick事件的写法总结 假设layout里有三个Button吧,id分别是 button_1 ,button_2 , button_3 之前一直都知道有两种onClick写法: button_1.setOnClickListener(new Button.OnClickListener(){ public void onClick(View v) { //在这里添加点击事件 } }); 第二种: button_2.setOnClickListener(liste

  • Android响应事件onClick方法的五种实现方式小结

    在Android的开发中,对于点击事件的OnClickListener有下面四种实现方式,可以根据实际场景的需要选择合适的用法.下面以Button按钮来举例说明. 方法一: 适合场景:任何场景都通用,但对于一个Activity中要是有多个控件要实现onClick方法就会显得代码冗余. Button bt_Demo = (Button)findViewById(R.id.bt_Demo); bt_Demo.setOnClickListener(new OnClickListener() { @Ov

  • Android onClick按钮单击事件的四种常用写法

    这里使用四种onClick单击事件,来实现电话拔号器的DEMO. XML文件 <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="phone" android:ems="10" android:id="@+id/et_number" android:l

  • android全局监控click事件的四种方式(小结)

    本文主要给大家分享如何在全局上去监听 click 点击事件,并做些通用处理或是拦截.使用场景可能就是具体的全局防快速重复点击,或是通用打点分析上报,用户行为监控等.以下将以四种不同的思路和实现方式去监控全局的点击操作,由简单到复杂逐一讲解. 方式一,适配监听接口,预留全局处理接口并作为所有监听器的基类使用 抽象出公共基类监听对象,可预留拦截机制和通用点击处理,简要代码如下: public abstract class CustClickListener implements View.OnCli

  • Javascript中绑定click事件的四种方式介绍

    一:HTML中添加onclick <button id="vv" onclick="myfunction()" >哈哈</button> 二:JS中定义函数绑定事件! var funcc = function () { alert('我爱编程') } var aa = document.getElementById('vv') aa.onclick = funcc 三:直接定义函数与内容 document.getElementById('vv

  • jQuery绑定事件的四种方式介绍

    jQuery提供了多种绑定事件的方式,每种方式各有其特点,明白了它们之间的异同点,有助于我们在写代码的时候进行正确的选择,从而写出优雅而容易维护的代码.下面我们来看下jQuery中绑定事件的方式都有哪些. jQuery中提供了四种事件监听方式,分别是bind.live.delegate.on,对应的解除监听的函数分别是unbind.die.undelegate.off.在开始看他们之前 一:bind(type,[data],function(eventObject)) bind是使用频率较高的一

  • springboot集成websocket的四种方式小结

    目录 1. 原生注解 2. Spring封装 3. TIO STOMP Session 共享的问题 如何选择 其它 参考链接 1. 原生注解 pom.xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> WebSocketConfi

  • Laravel 实现Controller向blade前台模板赋值的四种方式小结

    如下所示: <?php namespace App\Http\Controllers; use Illuminate\Http\Request; class TestController extends Controller { public function show(){ return view('show',['name'=>'asdfasdfasdfa']); //方法一 是把数组里的键值对赋值过去了,blade模板里用键名来取,如{{$name}} {{$gender}} //方法二

  • Springcloud+Mybatis使用多数据源的四种方式(小结)

    前段时间在做会员中心和中间件系统开发时,多次碰到多数据源配置问题,主要用到分包方式.参数化切换.注解+AOP.动态添加 这四种方式.这里做一下总结,分享下使用心得以及踩过的坑. 分包方式 数据源配置文件 在yml中,配置两个数据源,id分别为master和s1. spring: datasource: master: jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db1?......... username: xxx password: xxx drive

  • 基于spring三方包类注入容器的四种方式小结

    如果引用第三方jar包,肯定是不能直接使用常用注解@Controller.@Service.@Repository.@Component将类的实例注入到spring容器中.以下四种方法可以向spring容器中导入三方包中类实例 . 1 xml配置 这种情况大家用的比较多,就是在spring的xml文件中配置需要导入的bean.在springweb项目工程web.xml中 ContextLoaderListener或者DispatcherServlet的初始参数contextConfigLocat

  • Spring中集成Groovy的四种方式(小结)

    groovy是一种动态脚本语言,适用于一些可变.和规则配置性的需求,目前Spring提供ScriptSource接口,支持两种类型,一种是 ResourceScriptSource,另一种是 StaticScriptSource,但是有的场景我们需要把groovy代码放进DB中,所以我们需要扩展这个. ResourceScriptSource:在 resources 下面写groovy类 StaticScriptSource:把groovy类代码放进XML里 DatabaseScriptSour

  • Java 数组转List的四种方式小结

    目录 第一种方式(未必最佳):使用ArrayList.asList(strArray) 第二种方法(支持增删查改): 第三种方式(通过集合工具类Collections.addAll()方法(最高效)) 第四种方式通过JDK8的Stream流将3总基本类型数组转为List java数组转list误区 一.不能把基本数据类型转化为列表 二.asList方法返回的是数组的一个视图 第一种方式(未必最佳):使用ArrayList.asList(strArray) ​ 使用Arrays工具类Arrays.

  • 详解android进行异步更新UI的四种方式

    大家都知道由于性能要求,Android要求只能在UI线程中更新UI,要想在其他线程中更新UI,我大致总结了4种方式,欢迎补充纠正: 使用Handler消息传递机制: 使用AsyncTask异步任务: 使用runOnUiThread(action)方法: 使用Handler的post(Runnabel r)方法: 下面分别使用四种方式来更新一个TextView. 1.使用Handler消息传递机制 package com.example.runonuithreadtest; import andr

随机推荐