Android开发经验谈Android技术知识Android开发

Android | 使用 AspectJ 限制按钮快速点击(一)

2020-03-03  本文已影响0人  彭旭锐

前言


目录


1. 定义需求

在开始讲解之前,我们先定义需求,具体描述如下:

限制快速点击需求 示意图

2. 常规处理方法

目前比较常见的限制快速点击的处理方法有以下两种,具体如下:

2.1 封装代理类

封装一个代理类处理点击事件,代理类通过判断点击间隔决定是否拦截点击事件,具体代码如下:

// 代理类
public abstract class FastClickListener implements View.OnClickListener {
    private long mLastClickTime;
    private long interval = 1000L;

    public FastClickListener() {
    }

    public FastClickListener(long interval) {
        this.interval = interval;
    }

    @Override
    public void onClick(View v) {
        long currentTime = System.currentTimeMillis();
        if (currentTime - mLastClickTime > interval) {
            // 经过了足够长的时间,允许点击
            onClick();
            mLastClickTime = nowTime;
        } 
    }

    protected abstract void onClick();
}

在需要限制快速点击的地方使用该代理类,具体如下:

tv.setOnClickListener(new FastClickListener() {
    @Override
    protected void onClick() {
        // 处理点击逻辑
    }
});

2.2 RxAndroid 过滤表达式

使用RxJava的过滤表达式throttleFirst也可以限制快速点击,具体如下:

RxView.clicks(view)
    .throttleFirst(1, TimeUnit.SECONDS)
    .subscribe(new Consumer<Object>() {
        @Override
        public void accept(Object o) throws Exception {
            // 处理点击逻辑
        }
     });

2.3 小结

代理类RxAndroid过滤表达式这两种处理方法都存在两个缺点:

我们需要一种方案能够规避这两个缺点 —— AspectJAspectJ是一个流行的Java AOP(aspect-oriented programming)编程扩展框架,若还不了解,请务必查看文章:《Android | 一文带你全面了解 AspectJ 框架》


3. 详细步骤

在下面的内容里,我们将使用AspectJ框架,把限制快速点击的逻辑作为核心关注点从业务逻辑中抽离出来,单独维护。具体步骤如下:

步骤1:添加AspectJ依赖

// 项目级build.gradle
dependencies {
    classpath 'com.android.tools.build:gradle:3.5.3'
    classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
}

如果插件下载速度过慢,可以直接依赖插件 jar文件,将插件下载到项目根目录(如/plugins),然后在项目build.gradle中添加插件依赖:

// 项目级build.gradle
dependencies {
    classpath 'com.android.tools.build:gradle:3.5.3'
    classpath fileTree(dir:'plugins', include:['*.jar'])
}
// App Module的build.gradle
apply plugin: 'android-aspectjx'
...
// Module级build.gradle
dependencies {
    ...
    api 'org.aspectj:aspectjrt:1.8.9'
    ...
}

步骤2:实现判断快速点击的工具类

// ids.xml
<resources>
    <item type="id" name="view_click_time" />
</resources>
public class FastClickCheckUtil {

    /**
     * 判断是否属于快速点击
     *
     * @param view     点击的View
     * @param interval 快速点击的阈值
     * @return true:快速点击
     */
    public static boolean isFastClick(@NonNull View view, long interval) {
        int key = R.id.view_click_time;

        // 最近的点击时间
        long currentClickTime = System.currentTimeMillis();

        if(null == view.getTag(key)){
            // 1. 第一次点击

            // 保存最近点击时间
            view.setTag(key, currentClickTime);
            return false;
        }
        // 2. 非第一次点击

        // 上次点击时间
        long lastClickTime = (long) view.getTag(key);
        if(currentClickTime - lastClickTime < interval){
            // 未超过时间间隔,视为快速点击
            return true;
        }else{
            // 保存最近点击时间
            view.setTag(key, currentClickTime);
            return false;
        }
    }
}

步骤3:定义Aspect切面

使用@Aspect注解定义一个切面,使用该注解修饰的类会被AspectJ编译器识别为切面类:

@Aspect
public class FastClickCheckerAspect {
    // 随后填充
}

步骤4:定义PointCut切入点

使用@Pointcut注解定义一个切入点,编译期AspectJ编译器将搜索所有匹配的JoinPoint,执行织入:

@Aspect
public class FastClickAspect {

    // 定义一个切入点:View.OnClickListener#onClick()方法
    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void methodViewOnClick() {
    }

    // 随后填充 Advice
}

步骤5:定义Advice增强

增强的方式有很多种,在这里我们使用@Around注解定义环绕增强,它将包装PointCut,在PointCut前后增加横切逻辑,具体如下:

@Aspect
public class FastClickAspect {
    
    // 定义切入点:View.OnClickListener#onClick()方法
    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void methodViewOnClick() {}

    // 定义环绕增强,包装methodViewOnClick()切入点
    @Around("methodViewOnClick()")
    public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {
        // 取出目标对象
        View target = (View) joinPoint.getArgs()[0];
        // 根据点击间隔是否超过2000,判断是否为快速点击
        if (!FastClickCheckUtil.isFastClick(target, 2000)) {
            joinPoint.proceed();
        }
    }
}

步骤6:实现View.OnClickListener

在这一步我们为View设置OnClickListener,可以看到我们并没有添加限制快速点击的相关代码,增强的逻辑对原有逻辑没有侵入,具体代码如下:

// 源码:
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("AspectJ","click");
            }
        });
    }
}

编译代码,随后反编译AspectJ编译器执行织入后的.class文件。还不了解如何查找编译后的.class文件,请务必查看文章:《Android | 一文带你全面了解 AspectJ 框架》

public class MainActivity extends AppCompatActivity {
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(2131361820);
    findViewById(2131165349).setOnClickListener(new View.OnClickListener() {
          private static final JoinPoint.StaticPart ajc$tjp_0;
          
          // View.OnClickListener#onClick()
          public void onClick(View v) {
            View view = v;
            // 重构JoinPoint,执行环绕增强,也执行@Around修饰的方法
            JoinPoint joinPoint = Factory.makeJP(ajc$tjp_0, this, this, view);
            onClick_aroundBody1$advice(this, view, joinPoint, FastClickAspect.aspectOf(), (ProceedingJoinPoint)joinPoint);
          }
          
          static {
            ajc$preClinit();
          }
          
          private static void ajc$preClinit() {
            Factory factory = new Factory("MainActivity.java", null.class);
            ajc$tjp_0 = factory.makeSJP("method-execution", (Signature)factory.makeMethodSig("1", "onClick", "com.have.a.good.time.aspectj.MainActivity$1", "android.view.View", "v", "", "void"), 25);
          }
          
          // 原来在View.OnClickListener#onClick()中的代码,相当于核心业务逻辑
          private static final void onClick_aroundBody0(null ajc$this, View v, JoinPoint param1JoinPoint) {
            Log.i("AspectJ", "click");
          }
          
          // @Around方法中的代码,即源码中的aroundViewOnClick(),相当于Advice
          private static final void onClick_aroundBody1$advice(null ajc$this, View v, JoinPoint thisJoinPoint, FastClickAspect ajc$aspectInstance, ProceedingJoinPoint joinPoint) {
            View target = (View)joinPoint.getArgs()[0];
            if (!FastClickCheckUtil.isFastClick(target, 2000)) {
              // 非快速点击,执行点击逻辑
              ProceedingJoinPoint proceedingJoinPoint = joinPoint;
              onClick_aroundBody0(ajc$this, v, (JoinPoint)proceedingJoinPoint);
              null;
            } 
          }
        });
  }
}

小结

到这里,我们就讲解完使用AspectJ框架限制按钮快速点击的详细,总结如下:


4. 演进

现在,我们回归文章开头定义的需求,总共有4点。其中前两点使用目前的方案中已经能够实现,现在我们关注后面两点,即允许定制时间间隔覆盖尽可能多的点击场景

需求回归 示意图

4.1 定制时间间隔

在实际项目不同场景中的按钮,往往需要限制不同的点击时间间隔,因此我们需要有一种简便的方式用于定制不同场景的时间间隔,或者对于一些不需要限制快速点击的地方,有办法跳过快速点击判断,具体方法如下:

/**
 * 在需要定制时间间隔地方添加@FastClick注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FastClick {
    long interval() default FastClickAspect.FAST_CLICK_INTERVAL_GLOBAL;
}
@Aspect
public class SingleClickAspect {

    public static final long FAST_CLICK_INTERVAL_GLOBAL = 1000L;

    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void methodViewOnClick() {}

    @Around("methodViewOnClick()")
    public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {
        // 取出JoinPoint的签名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 取出JoinPoint的方法
        Method method = methodSignature.getMethod();

        // 1. 全局统一的时间间隔
        long interval = FAST_CLICK_INTERVAL_GLOBAL;

        if (method.isAnnotationPresent(FastClick.class)) {
            // 2. 如果方法使用了@FastClick修饰,取出定制的时间间隔

            FastClick singleClick = method.getAnnotation(FastClick.class);
            interval = singleClick.interval();
        }
        // 取出目标对象
        View target = (View) joinPoint.getArgs()[0];
        // 3. 根据点击间隔是否超过2000,判断是否为快速点击
        if (!FastClickCheckUtil.isFastClick(target, interval)) {
            joinPoint.proceed();
        }
    }
}
findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {
    @FastClick(interval = 5000L)
    @Override
    public void onClick(View v) {
        Log.i("AspectJ","click");
    }
});

4.2 完整场景覆盖

Editing...


上一篇下一篇

猜你喜欢

热点阅读