A simple way to block Android 's shake click event

code · 2020-02-23

Shake-Click is Always a trouble in Android development.
such as a button clicklistener implements in order jumping to another Activity.
It will seem like follow:

public class MainActivity extends AppCompatActivity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View view) {
        startActivity(new Intent(MainActivity.this,MainActivity.class));
      }
    });

  }
}

But if you clicking fast in a tiny duration , It will open two target Activity.

A simple way to avoid this issue is set the target launchMode to single top or single instance.

like this :

<activity android:name="debug.MainActivity"
            android:launchMode="singleInstance">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

But , if there is a great count of Activity in your Manifest ,it will be a 'treasure'
:smile

other simple way is add pre time check on click listener. like this:

public class MainActivity extends AppCompatActivity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener() {
      private long lastClickTime = 0;
      @Override public void onClick(View view) {
        if(System.currentTimeMillis() - lastClickTime > 1000){
          startActivity(new Intent(MainActivity.this,MainActivity.class));
        }
        lastClickTime = SystemClock.currentThreadTimeMillis();
      }
    });

  }
}

If your has a lot of OnClickListener it will be your disaster.
Here is a solution to resove the shake-double-click problems.

The Solution

All we know ,the View use TouchEvent to perform onClick Event, if the event can be hook or intercept in all view, the trouble can be done.

I found a hook-point seem to be very fitted to do this.
In View's class members , here is a Field named mAttachInfo. provided View's post() method implements. If you interest in read the fucking source , you can simply know the view's touch event will post a Runnable to Perform ClickEvent. the Runnable named android.view.View.PerformClick

here is the implement code :

package com.yixia.utils;

import android.app.Activity;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import java.lang.reflect.Field;
import java.util.HashMap;

/**
 * @author yixia zhangyanwei@yixia.com
 * @version version_code
 * @Copyright (c) 2017 miaopai
 * @Description
 * @date 2017/10/11
 */
public class DBClickBlocker extends Handler {
  private static final String TAG = "DBClickBlocker";
  private static final long DOUBLE_CLICK_TIME = 2000;
  private HashMap<Runnable, Long> mClickTimes = new HashMap<>();
  private Handler mOrigHandler = null;

  static {
    try {
      ATTACH_INFO_FIELD = View.class.getDeclaredField("mAttachInfo");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private static Field ATTACH_INFO_FIELD;

  public static void apply(Activity activity) {
    try {
      View decorView = activity.getWindow().getDecorView();
      if (ATTACH_INFO_FIELD == null) {
        Log.e(TAG, "un support device");
        return;
      }
      ATTACH_INFO_FIELD.setAccessible(true);
      Object attachInfo = ATTACH_INFO_FIELD.get(decorView);
      if (attachInfo == null && decorView != null) {
        decorView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
          @Override public void onViewAttachedToWindow(View v) {
            try {
              Object _attachInfo = ATTACH_INFO_FIELD.get(v);
              applyInner(_attachInfo);
            } catch (Exception e) {
              e.printStackTrace();
            }
          }

          @Override public void onViewDetachedFromWindow(View v) {
            Log.e(TAG, "remove callback success");
            v.removeOnAttachStateChangeListener(this);
          }
        });
      } else if (attachInfo != null && decorView != null) {
        applyInner(attachInfo);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  public static void apply(View view) {
    try {
      if (ATTACH_INFO_FIELD == null) {
        Log.e(TAG, "un support device");
        return;
      }
      ATTACH_INFO_FIELD.setAccessible(true);
      Object attachInfo = ATTACH_INFO_FIELD.get(view);
      if (attachInfo == null && view != null) {
        view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
          @Override public void onViewAttachedToWindow(View v) {
            try {
              Object _attachInfo = ATTACH_INFO_FIELD.get(v);
              applyInner(_attachInfo);
            } catch (Exception e) {
              e.printStackTrace();
            }
          }

          @Override public void onViewDetachedFromWindow(View v) {
            Log.e(TAG, "remove callback success");
            v.removeOnAttachStateChangeListener(this);
          }
        });
      } else if (attachInfo != null && view != null) {
        applyInner(attachInfo);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }


  private static void applyInner(Object attachInfo) throws Exception {
    if (attachInfo != null) {
      Field mHandler = attachInfo.getClass().getDeclaredField("mHandler");
      mHandler.setAccessible(true);
      Handler mAttachInfoHandler = (Handler) mHandler.get(attachInfo);
      mHandler.set(attachInfo, new DBClickBlocker(mAttachInfoHandler));
      Log.e(TAG, "install double blocker success !");
    }
  }

  public DBClickBlocker(Handler mOrigHandler) {
    this.mOrigHandler = mOrigHandler;
  }

  @Override public void dispatchMessage(Message msg) {
    mOrigHandler.dispatchMessage(msg);
  }

  @Override public String getMessageName(Message message) {
    return mOrigHandler.getMessageName(message);
  }

  @Override public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    if (msg.getCallback() != null && "android.view.View.PerformClick".equals(
        msg.getCallback().getClass().getCanonicalName())) {
      long currentTime = System.currentTimeMillis();
      if (mClickTimes.containsKey(msg.getCallback())) {
        if (currentTime - mClickTimes.get(msg.getCallback()) > DOUBLE_CLICK_TIME) {
          //超过双击时间,可以双击
          Log.e(TAG,"block over time...");
          mClickTimes.put(msg.getCallback(), currentTime);
          return mOrigHandler.sendMessageAtTime(msg, uptimeMillis);
        } else {
          Log.e(TAG,"block over success...");
          return true;
        }
      } else {
        Log.e(TAG,"block over new...");
        mClickTimes.put(msg.getCallback(), currentTime);
        return mOrigHandler.sendMessageAtTime(msg, uptimeMillis);
      }
    }
    return mOrigHandler.sendMessageAtTime(msg, uptimeMillis);
  }

  @Override public String toString() {
    return mOrigHandler.toString();
  }

  @Override public void handleMessage(Message msg) {
    mOrigHandler.handleMessage(msg);
  }
}

Update:

simple way to import dependencies
dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'com.android.support:appcompat-v7:24.2.0'
  compile 'io.zyw.os:fastclickblocker:1.1'
}

Usage:

DBClickBlocker.apply(Activty or View);

all is done.~

Sample

Android
Theme Jasmine by Kent Liao