天天看點

Android | 使用 AspectJ 限制按鈕快速點選

作者:彭醜醜

前言

在Android開發中,限制按鈕快速點選(按鈕防抖)是一個常見的需求;

在這篇文章裡,我将介紹一種使用AspectJ的方法,基于注解處理器 & 運作時注解反射的原理。如果能幫上忙,請務必點贊加關注,這真的對我非常重要。

目錄

Android | 使用 AspectJ 限制按鈕快速點選

1. 定義需求

在開始講解之前,我們先 定義需求,具體描述如下:

  • 限制快速點選需求 示意圖:
Android | 使用 AspectJ 限制按鈕快速點選

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過濾表達式​

​這兩種處理方法都存在兩個缺點:

  • 1. 侵入核心業務邏輯,需要将代碼替換到需要限制點選的地方;
  • 2. 修改工作量大,每一個增加限制點選的地方都要修改代碼。

我們需要一種方案能夠規避這兩個缺點 —— ​

​AspectJ​

​。 ​

​AspectJ​

​是一個流行的​

​Java​

​ ​

​AOP(aspect-oriented programming)​

​程式設計擴充架構,若還不了解,請務必檢視文章:《Android | 一文帶你全面了解 AspectJ 架構》

3. 詳細步驟

在下面的内容裡,我們将使用​

​AspectJ​

​架構,把限制快速點選的邏輯作為​

​核心關注點​

​從業務邏輯中抽離出來,單獨維護。具體步驟如下:

步驟1:添加​

​AspectJ​

​依賴

  1. 依賴滬江的​

    ​AspectJX​

    ​Gradle插件 —— 在項目​

    ​build.gradle​

    ​中添加插件依賴:
// 項目級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'])
}
      
  1. 應用插件 —— 在​

    ​App Module​

    ​的​

    ​build.gradle​

    ​中應用插件:
// App Module的build.gradle
apply plugin: 'android-aspectjx'
...
      
  1. 依賴AspectJ架構 —— 在包含​

    ​AspectJ​

    ​代碼的​

    ​Module​

    ​build.gradle​

    ​檔案中添加依賴:
// Module級build.gradle
dependencies {
    ...
    api 'org.aspectj:aspectjrt:1.8.9'
    ...
}
      

步驟2:實作判斷快速點選的工具類

  • 我們先實作一個判斷​

    ​View​

    ​是否快速點選的工具類;
  • 實作原理是使用​

    ​View​

    ​tag​

    ​屬性存儲最近一次的點選時間,每次點選時判斷目前時間距離存儲的時間是否已經經過了足夠長的時間;
  • 為了避免調用​

    ​View#setTag(int key,Object tag)​

    ​時傳入的​

    ​key​

    ​與其他地方傳入的​

    ​key​

    ​沖突而造成覆寫,務必使用在資源檔案中定義的 id,資源檔案中的 id 能夠有效保證全局唯一性,具體如下:
// 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架構​

​限制按鈕快速點選的詳細,總結如下:

  • ​@Aspect注解​

    ​描述一個​

    ​切面​

    ​AspectJ編譯器​

    ​識别為切面類;
  • ​@Pointcut注解​

    ​切入點​

    ​AspectJ編譯器​

    ​JoinPoint​

    ​,執行織入;
  • ​@Around注解​

    ​增強​

    ​,增強會被織入比對的​

    ​JoinPoint​

4. 演進

現在,我們回歸文章開頭定義的需求,總共有4點。其中前兩點使用目前的方案中已經能夠實作,現在我們關注後面兩點,即允許定制時間間隔與覆寫盡可能多的點選場景。

  • 需求回歸 示意圖:
Android | 使用 AspectJ 限制按鈕快速點選

4.1 定制時間間隔

在實際項目不同場景中的按鈕,往往需要限制不同的點選時間間隔,是以我們需要有一種簡便的方式用于定制不同場景的時間間隔,或者對于一些不需要限制快速點選的地方,有辦法跳過快速點選判斷,具體方法如下:

  • 定義注解
/**
 * 在需要定制時間間隔地方添加@FastClick注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FastClick {
    long interval() default FastClickAspect.FAST_CLICK_INTERVAL_GLOBAL;
}
      
  • 修改切面類的​

    ​Advice​

@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\. 根據點選間隔是否超過interval,判斷是否為快速點選
        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 完整場景覆寫

ButterKnife @OnClick android:onClick OK RecyclerView / ListView Java Lambda NO Kotlin Lambda OK DataBinding OK