Допустим, в приложении для Android мы хотим иметь возможность временно и надежно игнорировать все прикосновения пользователя в любой момент.
Из проведенного мной исследования переполнения стека, а также здесь, здесь и здесь, согласованное решение выглядит примерно так:
(Код MainActivity.java):
// returning true should mean touches are ignored/blocked
@Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (disableTouches) {
return true;
} else {
return super.dispatchTouchEvent(pEvent);
}
}
Однако, когда мы вводим Инструмент для упражнений Android Monkey и отправляем события касания в приложение с большой скоростью, становится очевидным, что свиньи начинают летать на квантовом уровне - мы можем получать звонки на onClick()
даже после / во время, когда «blockTouches» был установлено значение true.
МОЙ ВОПРОС: Почему? - Это нормальное поведение Android или я допустил ошибку в коде? :)
Примечание: Я уже исключил возможность того, что onClick()
будет вызван пользовательским вводом, отличным от прикосновений (и, следовательно, не будет контролироваться методом onDispatchTouchEvent()
) ... путем добавления «—-pct-touch 100» к команде обезьяны.
Вот код, который я использую для этого теста:
Основная деятельность:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
View rootView; // turns black when "touch rejection" is in progress
View allowedButton;
View notAllowedButton;
// Used to decide whether to process touch events.
// Set true temporarily when notAllowedButton is clicked.
boolean touchRejectionAnimationInProgress = false;
int errorCount = 0; // counting "unexpected/impossible" click calls
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootView = findViewById(R.id.rootView);
allowedButton = findViewById(R.id.allowedButton);
notAllowedButton = findViewById(R.id.notAllowedButton);
allowedButton.setOnClickListener(this);
notAllowedButton.setOnClickListener(this);
allowedButton.setBackgroundColor(Color.GREEN);
notAllowedButton.setBackgroundColor(Color.RED);
}
// returning true should mean touches are ignored/blocked
@Override
public boolean dispatchTouchEvent(MotionEvent pEvent) {
if (touchRejectionAnimationInProgress) {
Log.i("XXX", "touch rejected in dispatchTouchevent()");
return true;
} else {
return super.dispatchTouchEvent(pEvent);
}
}
@Override
public void onClick(View viewThatWasClicked){
Log.i("XXX", "onClick() called. View clicked: " + viewThatWasClicked.getTag());
//checking for unexpected/"impossible"(?) calls to this method
if (touchRejectionAnimationInProgress) {
Log.i("XXX!", "IMPOSSIBLE(?) call to onClick() detected.");
errorCount ++;
Log.i("XXX!", "Number of unexpected clicks: " + errorCount);
return;
} // else proceed...
if (viewThatWasClicked == allowedButton) {
// Irrelevant
} else if (viewThatWasClicked == notAllowedButton) {
// user did something that is not allowed.
touchRejectionAnimation();
}
}
// When the user clicks on something "illegal,"
// all user input is ignored temporarily for 200 ms.
// (arbitrary choice of duration, but smaller is better for testing)
private void touchRejectionAnimation() {
Log.i("XXX", "touchRejectionAnimation() called.");
touchRejectionAnimationInProgress = true;
rootView.setBackgroundColor(Color.BLACK);
// for logging/debugging purposes...
final String rejectionID = (new Random().nextInt() % 9999999) + "";
Log.i("XXX", "rejection : " + rejectionID + " started.");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try { Thread.sleep(200); } catch (Exception e) {
Log.e("XXX", "exception in touchRejection() BG thread!");
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.i("XXX", "rejection " + rejectionID + " ending");
rootView.setBackgroundColor(Color.WHITE);
touchRejectionAnimationInProgress = false;
}
});
}
});
thread.start();
}
}
layout.xml:
<?xml version = "1.0" encoding = "utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android = "http://schemas.android.com/apk/res/android"
xmlns:app = "http://schemas.android.com/apk/res-auto"
xmlns:tools = "http://schemas.android.com/tools"
android:id = "@+id/rootView"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
tools:context = ".MainActivity">
<View
android:id = "@+id/allowedButton"
android:layout_width = "0dp"
android:layout_height = "0dp"
android:layout_marginTop = "32dp"
android:layout_marginBottom = "32dp"
android:tag = "allowedButton"
app:layout_constraintBottom_toBottomOf = "parent"
app:layout_constraintEnd_toStartOf = "@+id/notAllowedButton"
app:layout_constraintStart_toStartOf = "parent"
app:layout_constraintTop_toTopOf = "parent" />
<View
android:id = "@+id/notAllowedButton"
android:layout_width = "0dp"
android:layout_height = "0dp"
android:layout_marginTop = "32dp"
android:layout_marginBottom = "32dp"
android:tag = "view2"
app:layout_constraintBottom_toBottomOf = "parent"
app:layout_constraintEnd_toEndOf = "parent"
app:layout_constraintStart_toEndOf = "@+id/allowedButton"
app:layout_constraintTop_toTopOf = "parent" />
</android.support.constraint.ConstraintLayout>
Он может щелкнуть и действительно щелкает по обоим из них по дизайну.
Если вы не хотите, чтобы ваш onClick () запускался при любом просмотре, щелкните.
Ниже приведены шаги, о которых необходимо позаботиться.
Create custom viewGroup eg: MyConstraintLayout and add all child inside it.
Override onInterceptTouchEvent(MotionEvent ev) and return it has true.
public class MyConstraintLayout extends ConstraintLayout {
private boolean mIsViewsTouchable;
public ParentView(Context context) {
super(context);
}
public ParentView(Context context, AttributeSet attrs) {
super(context, attrs);
inflate(context, R.layout.custom_view, this);
}
public ParentView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setViewsTouchable(boolean isViewTouchable) {
mIsViewsTouchable = isViewTouchable;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mIsViewsTouchable;
}
}
Примечание: используйте метод setViewsTouchable () в соответствии с вашими требованиями. Если вы передадите параметр как true, все представления не будут кликабельными, если false, ваши представления будут кликабельными.
Я не понимаю, почему это сработает, поскольку MainActivity.onDispatchTouchEvent () уже возвращает true в моем примере и существует вверх по течению (происходит раньше, чем) ViewGroup.onInterceptTouchEvent ... но я все равно попытался протестировать этот подход, который вы описываете ... но столкнулся с проблемой: я не могу понять, как адаптировать "inflate (context, R.layout.custom_view, this)" к моему варианту использования, где настраиваемый ConstraintLayout (MyConstraintLayout) будет корневым представлением activity_main .xml.
Вы уверены, что ваш сценарий обезьяны не нажимает на
allowedButton
иnotAllowedButton
? Можете ли вы полностью отключить их и попробовать еще раз?