前言
- 在
Android
開(kāi)發(fā)中,限制按鈕快速點(diǎn)擊(按鈕防抖)是一個(gè)常見(jiàn)的需求; - 在這篇文章里,我將介紹一種使用
AspectJ
的方法谭溉,基于注解處理器 & 運(yùn)行時(shí)注解反射的原理墙懂。如果能幫上忙橡卤,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注,這真的對(duì)我非常重要损搬。
系列文章
延伸文章
- 關(guān)于反射碧库,請(qǐng)閱讀:《Java | 反射:在運(yùn)行時(shí)訪問(wèn)類(lèi)型信息(含 Kotlin)》
- 關(guān)于注解,請(qǐng)閱讀:《Java | 這是一篇全面的注解使用攻略(含 Kotlin)》
- 關(guān)于注解處理器(APT)巧勤,請(qǐng)閱讀:《Java | 注解處理器(APT)原理解析 & 實(shí)踐》
目錄
1. 定義需求
在開(kāi)始講解之前嵌灰,我們先定義需求,具體描述如下:
2. 常規(guī)處理方法
目前比較常見(jiàn)的限制快速點(diǎn)擊的處理方法有以下兩種颅悉,具體如下:
2.1 封裝代理類(lèi)
封裝一個(gè)代理類(lèi)
處理點(diǎn)擊事件沽瞭,代理類(lèi)通過(guò)判斷點(diǎn)擊間隔決定是否攔截點(diǎn)擊事件,具體代碼如下:
// 代理類(lèi)
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) {
// 經(jīng)過(guò)了足夠長(zhǎng)的時(shí)間剩瓶,允許點(diǎn)擊
onClick();
mLastClickTime = nowTime;
}
}
protected abstract void onClick();
}
在需要限制快速點(diǎn)擊的地方使用該代理類(lèi)驹溃,具體如下:
tv.setOnClickListener(new FastClickListener() {
@Override
protected void onClick() {
// 處理點(diǎn)擊邏輯
}
});
2.2 RxAndroid 過(guò)濾表達(dá)式
使用RxJava
的過(guò)濾表達(dá)式throttleFirst
也可以限制快速點(diǎn)擊,具體如下:
RxView.clicks(view)
.throttleFirst(1, TimeUnit.SECONDS)
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
// 處理點(diǎn)擊邏輯
}
});
2.3 小結(jié)
代理類(lèi)
和RxAndroid過(guò)濾表達(dá)式
這兩種處理方法都存在兩個(gè)缺點(diǎn):
- 1. 侵入核心業(yè)務(wù)邏輯延曙,需要將代碼替換到需要限制點(diǎn)擊的地方豌鹤;
- 2. 修改工作量大,每一個(gè)增加限制點(diǎn)擊的地方都要修改代碼枝缔。
我們需要一種方案能夠規(guī)避這兩個(gè)缺點(diǎn) —— AspectJ
布疙。 AspectJ
是一個(gè)流行的Java
AOP(aspect-oriented programming)
編程擴(kuò)展框架,若還不了解愿卸,請(qǐng)務(wù)必查看文章:《Android | 一文帶你全面了解 AspectJ 框架》
3. 詳細(xì)步驟
在下面的內(nèi)容里灵临,我們將使用AspectJ
框架,把限制快速點(diǎn)擊的邏輯作為核心關(guān)注點(diǎn)
從業(yè)務(wù)邏輯中抽離出來(lái)趴荸,單獨(dú)維護(hù)儒溉。具體步驟如下:
步驟1:添加AspectJ
依賴(lài)
-
依賴(lài)滬江的
AspectJX
Gradle插件 —— 在項(xiàng)目build.gradle
中添加插件依賴(lài):
-
依賴(lài)滬江的
// 項(xiàng)目級(jí)build.gradle
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
}
如果插件下載速度過(guò)慢,可以直接依賴(lài)插件 jar文件赊舶,將插件下載到項(xiàng)目根目錄(如/plugins),然后在項(xiàng)目build.gradle
中添加插件依賴(lài):
// 項(xiàng)目級(jí)build.gradle
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath fileTree(dir:'plugins', include:['*.jar'])
}
-
應(yīng)用插件 —— 在
App Module
的build.gradle
中應(yīng)用插件:
-
應(yīng)用插件 —— 在
// App Module的build.gradle
apply plugin: 'android-aspectjx'
...
-
依賴(lài)AspectJ框架 —— 在包含
AspectJ
代碼的Module
的build.gradle
文件中添加依賴(lài):
-
依賴(lài)AspectJ框架 —— 在包含
// Module級(jí)build.gradle
dependencies {
...
api 'org.aspectj:aspectjrt:1.8.9'
...
}
步驟2:實(shí)現(xiàn)判斷快速點(diǎn)擊的工具類(lèi)
- 我們先實(shí)現(xiàn)一個(gè)判斷
View
是否快速點(diǎn)擊的工具類(lèi)睁搭; - 實(shí)現(xiàn)原理是使用
View
的tag
屬性存儲(chǔ)最近一次的點(diǎn)擊時(shí)間,每次點(diǎn)擊時(shí)判斷當(dāng)前時(shí)間距離存儲(chǔ)的時(shí)間是否已經(jīng)經(jīng)過(guò)了足夠長(zhǎng)的時(shí)間笼平; - 為了避免調(diào)用
View#setTag(int key,Object tag)
時(shí)傳入的key
與其他地方傳入的key
沖突而造成覆蓋园骆,務(wù)必使用在資源文件中定義的 id,資源文件中的 id 能夠有效保證全局唯一性寓调,具體如下:
// ids.xml
<resources>
<item type="id" name="view_click_time" />
</resources>
public class FastClickCheckUtil {
/**
* 判斷是否屬于快速點(diǎn)擊
*
* @param view 點(diǎn)擊的View
* @param interval 快速點(diǎn)擊的閾值
* @return true:快速點(diǎn)擊
*/
public static boolean isFastClick(@NonNull View view, long interval) {
int key = R.id.view_click_time;
// 最近的點(diǎn)擊時(shí)間
long currentClickTime = System.currentTimeMillis();
if(null == view.getTag(key)){
// 1. 第一次點(diǎn)擊
// 保存最近點(diǎn)擊時(shí)間
view.setTag(key, currentClickTime);
return false;
}
// 2. 非第一次點(diǎn)擊
// 上次點(diǎn)擊時(shí)間
long lastClickTime = (long) view.getTag(key);
if(currentClickTime - lastClickTime < interval){
// 未超過(guò)時(shí)間間隔锌唾,視為快速點(diǎn)擊
return true;
}else{
// 保存最近點(diǎn)擊時(shí)間
view.setTag(key, currentClickTime);
return false;
}
}
}
步驟3:定義Aspect
切面
使用@Aspect注解
定義一個(gè)切面
,使用該注解修飾的類(lèi)會(huì)被AspectJ編譯器
識(shí)別為切面類(lèi):
@Aspect
public class FastClickCheckerAspect {
// 隨后填充
}
步驟4:定義PointCut
切入點(diǎn)
使用@Pointcut注解
定義一個(gè)切入點(diǎn)
,編譯期AspectJ編譯器
將搜索所有匹配的JoinPoint
晌涕,執(zhí)行織入:
@Aspect
public class FastClickAspect {
// 定義一個(gè)切入點(diǎn):View.OnClickListener#onClick()方法
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void methodViewOnClick() {
}
// 隨后填充 Advice
}
步驟5:定義Advice
增強(qiáng)
增強(qiáng)的方式有很多種滋捶,在這里我們使用@Around注解
定義環(huán)繞增強(qiáng)
,它將包裝PointCut
余黎,在PointCut
前后增加橫切邏輯重窟,具體如下:
@Aspect
public class FastClickAspect {
// 定義切入點(diǎn):View.OnClickListener#onClick()方法
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void methodViewOnClick() {}
// 定義環(huán)繞增強(qiáng),包裝methodViewOnClick()切入點(diǎn)
@Around("methodViewOnClick()")
public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {
// 取出目標(biāo)對(duì)象
View target = (View) joinPoint.getArgs()[0];
// 根據(jù)點(diǎn)擊間隔是否超過(guò)2000惧财,判斷是否為快速點(diǎn)擊
if (!FastClickCheckUtil.isFastClick(target, 2000)) {
joinPoint.proceed();
}
}
}
步驟6:實(shí)現(xiàn)View.OnClickListener
在這一步我們?yōu)?code>View設(shè)置OnClickListener
巡扇,可以看到我們并沒(méi)有添加限制快速點(diǎn)擊的相關(guān)代碼,增強(qiáng)的邏輯對(duì)原有邏輯沒(méi)有侵入垮衷,具體代碼如下:
// 源碼:
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編譯器
執(zhí)行織入后的.class文件
。還不了解如何查找編譯后的.class文件
搀突,請(qǐng)務(wù)必查看文章:《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;
// 重構(gòu)JoinPoint刀闷,執(zhí)行環(huán)繞增強(qiáng),也執(zhí)行@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);
}
// 原來(lái)在View.OnClickListener#onClick()中的代碼仰迁,相當(dāng)于核心業(yè)務(wù)邏輯
private static final void onClick_aroundBody0(null ajc$this, View v, JoinPoint param1JoinPoint) {
Log.i("AspectJ", "click");
}
// @Around方法中的代碼甸昏,即源碼中的aroundViewOnClick(),相當(dāng)于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)) {
// 非快速點(diǎn)擊轩勘,執(zhí)行點(diǎn)擊邏輯
ProceedingJoinPoint proceedingJoinPoint = joinPoint;
onClick_aroundBody0(ajc$this, v, (JoinPoint)proceedingJoinPoint);
null;
}
}
});
}
}
小結(jié)
到這里筒扒,我們就講解完使用AspectJ框架
限制按鈕快速點(diǎn)擊的詳細(xì),總結(jié)如下:
- 使用
@Aspect注解
描述一個(gè)切面
绊寻,使用該注解修飾的類(lèi)會(huì)被AspectJ編譯器
識(shí)別為切面類(lèi)花墩; - 使用
@Pointcut注解
定義一個(gè)切入點(diǎn)
,編譯期AspectJ編譯器
將搜索所有匹配的JoinPoint
澄步,執(zhí)行織入冰蘑; - 使用
@Around注解
定義一個(gè)增強(qiáng)
,增強(qiáng)會(huì)被織入匹配的JoinPoint
4. 演進(jìn)
現(xiàn)在村缸,我們回歸文章開(kāi)頭定義的需求祠肥,總共有4點(diǎn)。其中前兩點(diǎn)使用目前的方案中已經(jīng)能夠?qū)崿F(xiàn)梯皿,現(xiàn)在我們關(guān)注后面兩點(diǎn)仇箱,即允許定制時(shí)間間隔與覆蓋盡可能多的點(diǎn)擊場(chǎng)景。
4.1 定制時(shí)間間隔
在實(shí)際項(xiàng)目不同場(chǎng)景中的按鈕东羹,往往需要限制不同的點(diǎn)擊時(shí)間間隔剂桥,因此我們需要有一種簡(jiǎn)便的方式用于定制不同場(chǎng)景的時(shí)間間隔,或者對(duì)于一些不需要限制快速點(diǎn)擊的地方属提,有辦法跳過(guò)快速點(diǎn)擊判斷权逗,具體方法如下:
- 定義注解
/**
* 在需要定制時(shí)間間隔地方添加@FastClick注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FastClick {
long interval() default FastClickAspect.FAST_CLICK_INTERVAL_GLOBAL;
}
- 修改切面類(lèi)的
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. 全局統(tǒng)一的時(shí)間間隔
long interval = FAST_CLICK_INTERVAL_GLOBAL;
if (method.isAnnotationPresent(FastClick.class)) {
// 2. 如果方法使用了@FastClick修飾美尸,取出定制的時(shí)間間隔
FastClick singleClick = method.getAnnotation(FastClick.class);
interval = singleClick.interval();
}
// 取出目標(biāo)對(duì)象
View target = (View) joinPoint.getArgs()[0];
// 3. 根據(jù)點(diǎn)擊間隔是否超過(guò)interval,判斷是否為快速點(diǎn)擊
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 完整場(chǎng)景覆蓋
ButterKnife @OnClick
android:onClick OK
RecyclerView / ListView
Java Lambda NO
Kotlin Lambda OK
DataBinding OK
Editting...
推薦閱讀
- 密碼學(xué) | Base64是加密算法嗎斟薇?
- 算法面試題 | 回溯算法解題框架
- 算法面試題 | 鏈表問(wèn)題總結(jié)
- Java | 帶你理解 ServiceLoader 的原理與設(shè)計(jì)思想
- 計(jì)算機(jī)網(wǎng)絡(luò) | 圖解 DNS & HTTPDNS 原理
- Android | 說(shuō)說(shuō)從 android:text 到 TextView 的過(guò)程
- Android | 面試必問(wèn)的 Handler师坎,你確定不看看?
- Android | 帶你探究 LayoutInflater 布局解析原理
- Android | View & Fragment & Window 的 getContext() 一定返回 Activity 嗎堪滨?