限制按钮点击_Android | 使用 AspectJ 限制按钮快速点击
前言
- 在Android開(kāi)發(fā)中,限制按鈕快速點(diǎn)擊(按鈕防抖)是一個(gè)常見(jiàn)的需求;
- 在這篇文章里,我將介紹一種使用AspectJ的方法,基于注解處理器 & 運(yùn)行時(shí)注解反射的原理。如果能幫上忙,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注,這真的對(duì)我非常重要。
系列文章
- 《Android | 一文帶你全面了解 AspectJ 框架》
- 《Android | 使用 AspectJ 限制按鈕快速點(diǎn)擊》
延伸文章
- 關(guān)于 反射,請(qǐng)閱讀:《Java | 反射:在運(yùn)行時(shí)訪問(wèn)類型信息(含 Kotlin)》
- 關(guān)于 注解,請(qǐng)閱讀:《Java | 這是一篇全面的注解使用攻略(含 Kotlin)》
- 關(guān)于 注解處理器(APT),請(qǐng)閱讀:《Java | 注解處理器(APT)原理解析 & 實(shí)踐》
目錄
1. 定義需求
在開(kāi)始講解之前,我們先 定義需求,具體描述如下:
- 限制快速點(diǎn)擊需求 示意圖:
2. 常規(guī)處理方法
目前比較常見(jiàn)的限制快速點(diǎn)擊的處理方法有以下兩種,具體如下:
2.1 封裝代理類
封裝一個(gè)代理類處理點(diǎn)擊事件,代理類通過(guò)判斷點(diǎn)擊間隔決定是否攔截點(diǎn)擊事件,具體代碼如下:
// 代理類 public abstract class FastClickListener implements View.OnClickListener {private long mLastClickTime;private long interval = 1000L;public FastClickListener() {}public FastClickListener(long interval) {this.interval = interval;}@Overridepublic 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)擊的地方使用該代理類,具體如下:
tv.setOnClickListener(new FastClickListener() {@Overrideprotected 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>() {@Overridepublic void accept(Object o) throws Exception {// 處理點(diǎn)擊邏輯}});2.3 小結(jié)
代理類和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依賴
- 1、依賴滬江的AspectJXGradle插件 —— 在項(xiàng)目build.gradle中添加插件依賴:
如果插件下載速度過(guò)慢,可以直接依賴插件 jar文件,將插件下載到項(xiàng)目根目錄(如/plugins),然后在項(xiàng)目build.gradle中添加插件依賴:
// 項(xiàng)目級(jí)build.gradle dependencies {classpath 'com.android.tools.build:gradle:3.5.3'classpath fileTree(dir:'plugins', include:['*.jar']) }- 2、應(yīng)用插件 —— 在App Module的build.gradle中應(yīng)用插件:
- 3、依賴AspectJ框架 —— 在包含AspectJ代碼的Module的build.gradle文件中添加依賴:
步驟2:實(shí)現(xiàn)判斷快速點(diǎn)擊的工具類
- 我們先實(shí)現(xiàn)一個(gè)判斷View是否快速點(diǎn)擊的工具類;
- 實(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 能夠有效保證全局唯一性,具體如下:
步驟3:定義Aspect切面
使用@Aspect注解定義一個(gè)切面,使用該注解修飾的類會(huì)被AspectJ編譯器識(shí)別為切面類:
@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)閂iew設(shè)置OnClickListener,可以看到我們并沒(méi)有添加限制快速點(diǎn)擊的相關(guān)代碼,增強(qiáng)的邏輯對(duì)原有邏輯沒(méi)有侵入,具體代碼如下:
// 源碼: public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {@Overridepublic 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)于Adviceprivate 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è)切面,使用該注解修飾的類會(huì)被AspectJ編譯器識(shí)別為切面類; - 使用@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; }- 修改切面類的Advice
- 使用注解
4.2 完整場(chǎng)景覆蓋
ButterKnife @OnClick android:onClick OK RecyclerView / ListView Java Lambda NO Kotlin Lambda OK DataBinding OK
Editting...
推薦閱讀
密碼學(xué) | Base64 是加密算法嗎??juejin.im算法面試題 | 回溯算法解題框架?juejin.im算法面試題 | 鏈表問(wèn)題總結(jié)?juejin.imJava | 帶你理解 ServiceLoader 的原理與設(shè)計(jì)思想?juejin.im計(jì)算機(jī)網(wǎng)絡(luò) | 圖解 DNS & HTTPDNS 原理?juejin.imAndroid | 說(shuō)說(shuō)從 android:text 到 TextView 的過(guò)程?juejin.imAndroid | 面試必問(wèn)的 Handler,你確定不看看??www.jianshu.comAndroid | 帶你探究 LayoutInflater 布局解析原理?juejin.imAndroid | View & Fragment & Window 的 getContext() 一定返回 Activity 嗎??juejin.im感謝喜歡!你的點(diǎn)贊是對(duì)我最大的鼓勵(lì)!歡迎關(guān)注彭旭銳的GitHub!
總結(jié)
以上是生活随笔為你收集整理的限制按钮点击_Android | 使用 AspectJ 限制按钮快速点击的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Pycharm 输出中文或打印中文乱码现
- 下一篇: 从一个Android码农视角回顾2018