萬萬沒想到,做防重複點擊都會被坑4下
- 2020 年 2 月 23 日
- 筆記
前幾天,發現App設置頁中有一堆的入口,點擊一些item快點會啟動兩個頁,舉個例子,就比如說微信這個發現頁:

這裡,點擊每個入口都會進入一個新的Activity,但是,如果快速點擊的話,比如快速點擊附近的人,將會出現兩個附近的人頁。
因此,我們要如何解決這個問題呢?
- 將所有的Activity設置為singleTop
- 對附近的人這個按鈕的onClick事件做一個防止重複點擊
兩種方式都是沒問題的,但是,卻都有問題,首頁我們來分析第一種:
將所有的Activity設置為singleTop
為什麼說這種方式有問題,首先,我們要了解singleTop啟動模式是幹嘛,他是說,如果當前Activity已經在棧頂了,那麼,就不在啟動一個新的這個Activity,只是調用它的onNewIntent
,我們能排除一定不會在棧頂已經有這個Activity的時候,在開同樣的頁面嗎?不能!業務千變萬化。
那麼,singleTask可以嗎?抱歉更加不行,singleTask表示如果這個頁面棧中有這個Activity的話,就復用它,並且幹掉處在它上面的所有Activity,讓自己處於棧頂,妥妥的踢人上位,誰,因此,我們更不可能將所有的Activity設置為singleTask模式了。
所以,第一種方式弊端很明顯:
- 我們不能為了方式用會多開頁面,就以偏概全,將所有的Activity設置為singleTop。
- 而且,這麼做局限性很大,因為沒有看到問題的本質,問題的本質是因為onClick執行兩次造成的,而出現兩個Activity只是結果。
- 我們卻要對這個結果進行容錯,而不是針對引發這個現象的源頭進行處理,就有點本末倒置了。
針對這個按鈕的onClick事件做一個防止重複點擊
嗯,這回看似已經找到了問題造成的根源了,如是,你這麼寫:
btNeayby.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { long nowTime = System.currentTimeMillis(); if (nowTime - mLastClickTime > TIME_INTERVAL) { enterActiviy() } } }); }
一些變數就不在這裡給出了,相信你也能看懂這個邏輯,對一處點擊能起到防止重複點擊的效果,那麼,其他地方呢?其他地方你都要寫這樣一段邏輯,都要定義一個最後一次點擊的時間,好麻煩~~
所以,有沒有辦法,不用去定義這些變數,去寫包裹邏輯,回答是有的
RxView.clicks(view) .throttleFirst(1, TimeUnit.SECONDS) .subscribe(new Consumer<Object>() { @Override public void accept(Object o) throws Exception { enterActiviy() } });
嗯,看起來貌似是可以了,比第一版簡潔不少,沒有mLastClickTime變數的定義了,但是,項目中肯定有很多地方需要點擊事件的,難不成,你每個地方都用RxView.clicks去包裹一遍 所以,有沒有再簡潔一點的呢,答案是有的
Android APT(編譯時程式碼生成),相信對這個有所了解的小夥伴大概知道我會說什麼了?如果你還不了解這個灰科技,可以看看這篇文章 Android APT(編譯時程式碼生成)最佳實踐
解決思路我幫你理一理:
- 定義一個註解Annotation,比如就叫做SingleClick
- 有了APT這個灰科技,在編譯時根據這個Annotation生成了相關的程式碼。
相信了解過ButterKnife的同學應該知道:
@OnClick(R.id.bt_submit) public void submit() { title.setText("hello world"); }
這個註解,實際上他做了什麼事呢?
- 生成程式碼將R.id.bt_submit 通過findViewBy()綁定到一個變數,比如
mSubmit
上來。 - 給
mSubmit
設置onClick事件。 - 在onClick事件的處理中,將處理權轉發給submit這個被onClick註解方法處理而已
@Override public void onClick(View v) { Method method = null; try { method = receiver.getClass().getMethod(clickMethodName); if (method != null) { method.invoke(receiver); } } catch (Exception e) { e.printStackTrace(); Log.e(TAG, "未找到:" + clickMethodName + "方法"); } try { if (method == null) { method = receiver.getClass().getMethod(clickMethodName, View.class); if (method != null) { method.invoke(receiver, v); } } } catch (Exception e) { e.printStackTrace(); Log.e(TAG, "未找到帶view類型參數的:" + clickMethodName + "方法"); } }
只是,ButterKnife的OnClick註解並沒有做防重複點擊。
ButterKnife他沒做防重複的事情你可不可以加,當然是可以的
加了之後,是不是可以寫成這樣子了?
@SingleClick(R.id.bt_submit) public void submit() { title.setText("hello world"); }
然後submit是被被轉發過來,就看你APT的邏輯了。
眼看都到了這個份上了,就這樣玩了嗎?當然還沒有,我們:
我們還不滿足,因為,加入老子就是不喜歡用APT框架怎麼辦?就是不喜歡自己寫view.setOnlickListenser(...)
我們最終祭出終極大殺器,AOP
可能,知道點AOP的同學就秒懂了,沒錯,面向切面編程,我們為什麼不攔截onClick做點文章呢?
在想到這個方案之後,我就搜索了一下github,果然不出所料,有小夥伴就用這種方式處理了,GitHub – jarryleo/SingleClick: Android點擊事件防重庫
不過,我看到了我不大喜歡的地方,既然老子都用AOP了,幹嘛還喲啊在定義一個註解SingleClick呢?我為什麼不直接攔截所有的onClick呢?
如是,我的方案是:
@Aspect public class OnClickAspect { @Pointcut("execution(* onClick(..))") public void onClickPointcut() { } @Around("onClickPointcut()") public void onClick(ProceedingJoinPoint joinPoint) throws Throwable { // 取出方法的參數 ,想判是不是 onClick(View viw)這種類型的方法 View view = null; for (Object arg : joinPoint.getArgs()) { if (arg instanceof View) { view = (View) arg; break; } } if (view == null) { joinPoint.proceed(); return; } // 取出方法的註解,如果標記可以多次點擊的,就直接走 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); if (method.isAnnotationPresent(MutilClick.class)){ joinPoint.proceed(); return; } if (!XClickUtil.isFastDoubleClick(joinPoint.getTarget(),view, 500)) { // 不是快速點擊,執行原方法 joinPoint.proceed(); } } }
當然,我在做的過程中,也是發現了4個坑:
- 有些地方的點擊需要多次點擊怎麼辦?
- 如果在onClick事件中做了轉發怎麼處理?
- 如果出現super.onClick(v)怎麼處理?
- 打release包就出現NPE了怎麼處理?
以上的第一個問題是客觀存在的,比如,我們連續點擊一個按鈕幾次,彈出我們的後門,因此,我加了一個MutilClick的註解,來規避這種情況,這種情況極少,可能一兩處而已。
然而對於
onClick事件中做了轉發
view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { listener.onClick(v); } });
哈哈,你妹啊,這不就是活生生的onClick(v)被瞬間就調了兩次,妥妥的重複點擊了,這肯定就造成頁面上這種情況的按鈕無法點了,怎麼處理,別急,我們發現調用主體不同。實際上這種情況等同於:
A.click(view1) B.click(view1)
因此,可以判斷一下調用的主體是否一致,具體方法下面會給出。
super.onClick(v)
@Override public void onClick(View view) { super.onClick(view); switch (view.getId()) {
尷尬了吧,這種時候調用的主體都變成了一個,其實就等於
A.click(view1) A.click(view1)
啥都一樣,不一樣的就是先後各了幾ms而已,等等,人的手速可能幾ms嗎?顯然是不可能的,因此,我們似乎又找到了路子,所以總結起來,我們的防重複點擊工具類可以這麼寫:
package com.tencent.igame.common.utils; import android.text.TextUtils; import android.util.Log; import android.view.View; public class XClickUtil { /** * 最近一次發生事件的target */ private static String mLastTargetName; /** * 最近一次點擊的時間 */ private static long mLastClickTime; /** * 最近一次點擊的控制項ID */ private static int mLastClickViewId; /** * 是否是快速點擊 * * @param v 點擊的控制項 * @param intervalMillis 時間間期(毫秒) * @return true:是,false:不是 */ public static boolean isFastDoubleClick(Object target, View v, long intervalMillis) { int viewId = v.getId(); long time = System.currentTimeMillis(); long timeInterval = Math.abs(time - mLastClickTime); //10,表示手速不可能這麼快,突破ms if (timeInterval>10 && timeInterval < intervalMillis && viewId == mLastClickViewId && TextUtils.equals(getTargetHash(target), mLastTargetName)) { Log.e("XClickUtil", "重複點擊 target = [" + getTargetHash(target) + "], v = [" + v.getId() + "], currentTimeMillis = [" + time + "]"); return true; } else { // fixme 這裡其實可以加一下自動埋點 Log.e("XClickUtil", "單次點擊 target = [" + getTargetHash(target) + "], v = [" + v.getId() + "], currentTimeMillis = [" + time + "]"); mLastTargetName = getTargetHash(target); mLastClickTime = time; mLastClickViewId = viewId; return false; } } private static String getTargetHash(Object object) { return object.getClass().getName() + "@" + object.hashCode(); } }
最後一個坑,打release包直接就NPE
這種情況肯定就是混淆導致的了,一般加上混淆配置就OK了
#-------------------------註解AOP---------------------- -adaptclassstrings -keepattributes InnerClasses, EnclosingMethod, Signature, *Annotation* -keepnames @org.aspectj.lang.annotation.Aspect class * { ajc* <methods>; }