Merge "Refactor business logic out of SpeedBumpView." into oc-mr1-jetpack-dev
diff --git a/car/src/main/java/androidx/car/moderator/SpeedBumpController.java b/car/src/main/java/androidx/car/moderator/SpeedBumpController.java
new file mode 100644
index 0000000..750a967
--- /dev/null
+++ b/car/src/main/java/androidx/car/moderator/SpeedBumpController.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.moderator;
+
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.os.Handler;
+import android.support.annotation.VisibleForTesting;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+
+import androidx.car.R;
+
+/**
+ * A controller for the actual monitoring of when interaction should be allowed in a
+ * {@link SpeedBumpView}.
+ */
+class SpeedBumpController {
+ /**
+ * The number of permitted actions that are acquired per second that the user has not
+ * interacted with the {@code SpeedBumpView}.
+ */
+ private static final float ACQUIRED_PERMITS_PER_SECOND = 0.5f;
+
+ /** The maximum number of permits that can be acquired when the user is idling. */
+ @VisibleForTesting
+ static final float MAX_PERMIT_POOL = 5f;
+
+ /** The delay between when the permit pool has been depleted and when it begins to refill. */
+ private static final long PERMIT_FILL_DELAY_MS = 600L;
+
+ private final ContentRateLimiter mContentRateLimiter = new ContentRateLimiter(
+ ACQUIRED_PERMITS_PER_SECOND,
+ MAX_PERMIT_POOL,
+ PERMIT_FILL_DELAY_MS);
+
+ /**
+ * Whether or not the user is currently allowed to interact with any child views of
+ * {@code SpeedBumpView}.
+ */
+ private boolean mInteractionPermitted = true;
+
+ private final int mLockOutMessageDurationMs;
+ private final Handler mHandler = new Handler();
+
+ private final Context mContext;
+ private final View mLockoutMessageView;
+ private final ImageView mLockoutImageView;
+
+ /**
+ * Creates the {@code SpeedBumpController} and associate it with the given
+ * {@code SpeedBumpView}.
+ */
+ SpeedBumpController(SpeedBumpView speedBumpView) {
+ mContext = speedBumpView.getContext();
+
+ LayoutInflater layoutInflater = LayoutInflater.from(mContext);
+ mLockoutMessageView =
+ layoutInflater.inflate(R.layout.lock_out_message, speedBumpView, false);
+ mLockoutImageView = mLockoutMessageView.findViewById(R.id.lock_out_drawable);
+ mLockOutMessageDurationMs =
+ mContext.getResources().getInteger(R.integer.speed_bump_lock_out_duration_ms);
+ }
+
+ /**
+ * Returns the view that is used by this {@code SpeedBumpController} for displaying a lock-out
+ * message saying that further interaction is blocked.
+ *
+ * @return The view that contains the lock-out message.
+ */
+ View getLockoutMessageView() {
+ return mLockoutMessageView;
+ }
+
+ /**
+ * Notifies this {@code SpeedBumpController} that the given {@link MotionEvent} has occurred.
+ * This method will return whether or not further interaction should be allowed.
+ *
+ * @param ev The {@link MotionEvent} that represents a touch event.
+ * @return {@code true} if the touch event should be allowed.
+ */
+ boolean onTouchEvent(MotionEvent ev) {
+ int action = ev.getActionMasked();
+
+ // Check if the user has just finished an MotionEvent and count that as an action. Check
+ // the ContentRateLimiter to see if interaction is currently permitted.
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ boolean nextActionPermitted = mContentRateLimiter.tryAcquire();
+
+ // Indicates that this is the first action that is not permitted. In this case, the
+ // child view should at least handle the ACTION_CANCEL or ACTION_UP, so call
+ // super.dispatchTouchEvent(), but lock out further interaction.
+ if (mInteractionPermitted && !nextActionPermitted) {
+ mInteractionPermitted = false;
+ showLockOutMessage();
+ return true;
+ }
+ }
+
+ // Otherwise, return if interaction is permitted.
+ return mInteractionPermitted;
+ }
+
+ /**
+ * Displays a message that informs the user that they are not permitted to interact any further
+ * with the current view.
+ */
+ private void showLockOutMessage() {
+ // If the message is visible, then it's already showing or animating in. So, do nothing.
+ if (mLockoutMessageView.getVisibility() == View.VISIBLE) {
+ return;
+ }
+
+ Animation lockOutMessageIn =
+ AnimationUtils.loadAnimation(mContext, R.anim.lock_out_message_in);
+ lockOutMessageIn.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ mLockoutMessageView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // When the lock-out message is completely shown, let it display for
+ // mLockOutMessageDurationMs milliseconds before hiding it.
+ mHandler.postDelayed(SpeedBumpController.this::hideLockOutMessage,
+ mLockOutMessageDurationMs);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+
+ mLockoutMessageView.clearAnimation();
+ mLockoutMessageView.startAnimation(lockOutMessageIn);
+ ((AnimatedVectorDrawable) mLockoutImageView.getDrawable()).start();
+ }
+
+ /**
+ * Hides any lock-out messages. Once the message is hidden, interaction with the view is
+ * permitted.
+ */
+ private void hideLockOutMessage() {
+ if (mLockoutMessageView.getVisibility() != View.VISIBLE) {
+ return;
+ }
+
+ Animation lockOutMessageOut =
+ AnimationUtils.loadAnimation(mContext, R.anim.lock_out_message_out);
+ lockOutMessageOut.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mLockoutMessageView.setVisibility(View.GONE);
+ mInteractionPermitted = true;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mLockoutMessageView.startAnimation(lockOutMessageOut);
+ }
+}
diff --git a/car/src/main/java/androidx/car/moderator/SpeedBumpView.java b/car/src/main/java/androidx/car/moderator/SpeedBumpView.java
index 81a97ee..ad65c6a 100644
--- a/car/src/main/java/androidx/car/moderator/SpeedBumpView.java
+++ b/car/src/main/java/androidx/car/moderator/SpeedBumpView.java
@@ -17,19 +17,12 @@
package androidx.car.moderator;
import android.content.Context;
-import android.graphics.drawable.AnimatedVectorDrawable;
-import android.os.Handler;
+import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
-import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-import androidx.car.R;
/**
* A wrapping view that will monitor all touch events on its children views and prevent the user
@@ -40,66 +33,36 @@
* message explaining that they are no longer able to interact with the view is also displayed.
*/
public class SpeedBumpView extends FrameLayout {
- /**
- * The number of permitted actions that are acquired per second that the user has not
- * interacted with the {@code SpeedBumpView}.
- */
- private static final float ACQUIRED_PERMITS_PER_SECOND = 0.5f;
-
- /** The maximum number of permits that can be acquired when the user is idling. */
- private static final float MAX_PERMIT_POOL = 5f;
-
- /** The delay between when the permit pool has been depleted and when it begins to refill. */
- private static final long PERMIT_FILL_DELAY_MS = 600L;
-
- private int mLockOutMessageDurationMs;
-
- private final ContentRateLimiter mContentRateLimiter = new ContentRateLimiter(
- ACQUIRED_PERMITS_PER_SECOND,
- MAX_PERMIT_POOL,
- PERMIT_FILL_DELAY_MS);
-
- /**
- * Whether or not the user is currently allowed to interact with any child views of
- * {@code SpeedBumpView}.
- */
- private boolean mInteractionPermitted = true;
-
- private final Handler mHandler = new Handler();
-
- private View mLockoutMessageView;
- private ImageView mLockoutImageView;
+ private final SpeedBumpController mSpeedBumpController;
public SpeedBumpView(Context context) {
super(context);
- init();
}
public SpeedBumpView(Context context, AttributeSet attrs) {
super(context, attrs);
- init();
}
public SpeedBumpView(Context context, AttributeSet attrs, int defStyleAttrs) {
super(context, attrs, defStyleAttrs);
- init();
}
public SpeedBumpView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
super(context, attrs, defStyleAttrs, defStyleRes);
- init();
}
- private void init() {
- mLockOutMessageDurationMs =
- getResources().getInteger(R.integer.speed_bump_lock_out_duration_ms);
+ {
+ mSpeedBumpController = new SpeedBumpController(this);
+ addView(mSpeedBumpController.getLockoutMessageView());
+ }
- LayoutInflater layoutInflater = LayoutInflater.from(getContext());
- mLockoutMessageView = layoutInflater.inflate(R.layout.lock_out_message, this, false);
- mLockoutImageView = mLockoutMessageView.findViewById(R.id.lock_out_drawable);
-
- addView(mLockoutMessageView);
- mLockoutMessageView.bringToFront();
+ /**
+ * Returns the view that is responsible for displaying a message telling the user that they
+ * have been locked out from further interaction.
+ */
+ @VisibleForTesting
+ View getLockOutMessageView() {
+ return mSpeedBumpController.getLockoutMessageView();
}
@Override
@@ -108,93 +71,13 @@
// Always ensure that the lock out view has the highest Z-index so that it will show
// above all other views.
- mLockoutMessageView.bringToFront();
+ mSpeedBumpController.getLockoutMessageView().bringToFront();
}
// Overriding dispatchTouchEvent to intercept all touch events on child views.
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
- int action = ev.getActionMasked();
-
- // Check if the user has just finished an MotionEvent and count that as an action. Check
- // the ContentRateLimiter to see if interaction is currently permitted.
- if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
- boolean nextActionPermitted = mContentRateLimiter.tryAcquire();
-
- // Indicates that this is the first action that is not permitted. In this case, the
- // child view should at least handle the ACTION_CANCEL or ACTION_UP, so call
- // super.dispatchTouchEvent(), but lock out further interaction.
- if (mInteractionPermitted && !nextActionPermitted) {
- mInteractionPermitted = false;
- showLockOutMessage();
- return super.dispatchTouchEvent(ev);
- }
- }
-
- // Otherwise, if interaction permitted, allow child views to handle touch events.
- return mInteractionPermitted && super.dispatchTouchEvent(ev);
- }
-
- /**
- * Displays a message that informs the user that they are not permitted to interact any further
- * with the current view.
- */
- private void showLockOutMessage() {
- // If the message is visible, then it's already showing or animating in. So, do nothing.
- if (mLockoutMessageView.getVisibility() == VISIBLE) {
- return;
- }
-
- Animation lockOutMessageIn =
- AnimationUtils.loadAnimation(getContext(), R.anim.lock_out_message_in);
- lockOutMessageIn.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationStart(Animation animation) {
- mLockoutMessageView.setVisibility(VISIBLE);
- }
-
- @Override
- public void onAnimationEnd(Animation animation) {
- // When the lock-out message is completely shown, let it display for
- // mLockOutMessageDurationMs milliseconds before hiding it.
- mHandler.postDelayed(SpeedBumpView.this::hideLockOutMessage,
- mLockOutMessageDurationMs);
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) {}
- });
-
- mLockoutMessageView.clearAnimation();
- mLockoutMessageView.startAnimation(lockOutMessageIn);
- ((AnimatedVectorDrawable) mLockoutImageView.getDrawable()).start();
- }
-
- /**
- * Hides any lock-out messages. Once the message is hidden, interaction with the view is
- * permitted.
- */
- private void hideLockOutMessage() {
- if (mLockoutMessageView.getVisibility() != VISIBLE) {
- return;
- }
-
- Animation lockOutMessageOut =
- AnimationUtils.loadAnimation(getContext(), R.anim.lock_out_message_out);
- lockOutMessageOut.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationStart(Animation animation) {}
-
- @Override
- public void onAnimationEnd(Animation animation) {
- mLockoutMessageView.setVisibility(GONE);
- mInteractionPermitted = true;
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) {}
- });
- mLockoutMessageView.startAnimation(lockOutMessageOut);
+ return mSpeedBumpController.onTouchEvent(ev) && super.dispatchTouchEvent(ev);
}
}