作者:彭醜醜
前言
在Android開發中,限制按鈕快速點選(按鈕防抖)是一個常見的需求;
在這篇文章裡,我将介紹一種使用AspectJ的方法,基于注解處理器 & 運作時注解反射的原理。如果能幫上忙,請務必點贊加關注,這真的對我非常重要。
目錄
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過濾表達式
這兩種處理方法都存在兩個缺點:
- 1. 侵入核心業務邏輯,需要将代碼替換到需要限制點選的地方;
- 2. 修改工作量大,每一個增加限制點選的地方都要修改代碼。
我們需要一種方案能夠規避這兩個缺點 ——
AspectJ
。
AspectJ
是一個流行的
Java
AOP(aspect-oriented programming)
程式設計擴充架構,若還不了解,請務必檢視文章:《Android | 一文帶你全面了解 AspectJ 架構》
3. 詳細步驟
在下面的内容裡,我們将使用
AspectJ
架構,把限制快速點選的邏輯作為
核心關注點
從業務邏輯中抽離出來,單獨維護。具體步驟如下:
步驟1:添加 AspectJ
依賴
AspectJ
- 依賴滬江的
Gradle插件 —— 在項目AspectJX
中添加插件依賴: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'])
}
- 應用插件 —— 在
的App Module
中應用插件:build.gradle
// App Module的build.gradle
apply plugin: 'android-aspectjx'
...
- 依賴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
沖突而造成覆寫,務必使用在資源檔案中定義的 id,資源檔案中的 id 能夠有效保證全局唯一性,具體如下:key
// 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
@Aspect注解
定義一個
切面
,使用該注解修飾的類會被
AspectJ編譯器
識别為切面類:
@Aspect
public class FastClickCheckerAspect {
// 随後填充
}
步驟4:定義 PointCut
切入點
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
增強
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點。其中前兩點使用目前的方案中已經能夠實作,現在我們關注後面兩點,即允許定制時間間隔與覆寫盡可能多的點選場景。
- 需求回歸 示意圖:
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