diff options
10 files changed, 1295 insertions, 19 deletions
diff --git a/packages/SystemUI/res/layout/invocation_lights.xml b/packages/SystemUI/res/layout/invocation_lights.xml new file mode 100644 index 000000000000..ff78670d0719 --- /dev/null +++ b/packages/SystemUI/res/layout/invocation_lights.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2019 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 + --> + +<com.android.systemui.assist.ui.InvocationLightsView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:visibility="gone"/> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 3f84b32ee0c2..abf4fdf03b93 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -166,14 +166,17 @@ <color name="smart_reply_button_stroke">#ffdadce0</color> <!-- Biometric dialog colors --> - <color name="biometric_dialog_dim_color">#80000000</color> <!-- 50% black --> + <color name="biometric_dialog_dim_color">#80000000</color> <!-- 50% black --> <color name="biometric_dialog_gray">#ff757575</color> - <color name="biometric_dialog_accent">#ff008577</color> <!-- dark teal --> - <color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 --> + <color name="biometric_dialog_accent">#ff008577</color> <!-- dark teal --> + <color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 --> <!-- Logout button --> <color name="logout_button_bg_color">#ccffffff</color> + <!-- Color for the Assistant invocation lights --> + <color name="default_invocation_lights_color">#ffffffff</color> <!-- white --> + <!-- GM2 colors --> <color name="GM2_grey_50">#F8F9FA</color> <color name="GM2_grey_100">#F1F3F4</color> diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index 2c38e513d7de..bca96623a4b6 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -40,8 +40,11 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.settingslib.applications.InterestingConfigChanges; import com.android.systemui.ConfigurationChangedReceiver; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SysUiServiceProvider; +import com.android.systemui.assist.ui.DefaultUiController; +import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -50,6 +53,40 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; */ public class AssistManager implements ConfigurationChangedReceiver { + /** + * Controls the UI for showing Assistant invocation progress. + */ + public interface UiController { + /** + * Updates the invocation progress. + * + * @param type one of INVOCATION_TYPE_GESTURE, INVOCATION_TYPE_ACTIVE_EDGE, + * INVOCATION_TYPE_VOICE, INVOCATION_TYPE_QUICK_SEARCH_BAR, + * INVOCATION_HOME_BUTTON_LONG_PRESS + * @param progress a float between 0 and 1 inclusive. 0 represents the beginning of the + * gesture; 1 represents the end. + */ + void onInvocationProgress(int type, float progress); + + /** + * Called when an invocation gesture completes. + * + * @param velocity the speed of the invocation gesture, in pixels per millisecond. For + * drags, this is 0. + */ + void onGestureCompletion(float velocity); + + /** + * Called with the Bundle from VoiceInteractionSessionListener.onSetUiHints. + */ + void processBundle(Bundle hints); + + /** + * Hides the UI. + */ + void hide(); + } + private static final String TAG = "AssistManager"; // Note that VERBOSE logging may leak PII (e.g. transcription contents). @@ -76,6 +113,7 @@ public class AssistManager implements ConfigurationChangedReceiver { private final InterestingConfigChanges mInterestingConfigChanges; private final PhoneStateMonitor mPhoneStateMonitor; private final AssistHandleBehaviorController mHandleController; + private final UiController mUiController; private AssistOrbContainer mView; private final DeviceProvisionedController mDeviceProvisionedController; @@ -85,16 +123,16 @@ public class AssistManager implements ConfigurationChangedReceiver { private IVoiceInteractionSessionShowCallback mShowCallback = new IVoiceInteractionSessionShowCallback.Stub() { - @Override - public void onFailed() throws RemoteException { - mView.post(mHideRunnable); - } + @Override + public void onFailed() throws RemoteException { + mView.post(mHideRunnable); + } - @Override - public void onShown() throws RemoteException { - mView.post(mHideRunnable); - } - }; + @Override + public void onShown() throws RemoteException { + mView.post(mHideRunnable); + } + }; private Runnable mHideRunnable = new Runnable() { @Override @@ -119,6 +157,23 @@ public class AssistManager implements ConfigurationChangedReceiver { | ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS); onConfigurationChanged(context.getResources().getConfiguration()); mShouldEnableOrb = !ActivityManager.isLowRamDeviceStatic(); + + mUiController = new DefaultUiController(mContext); + + OverviewProxyService overviewProxy = Dependency.get(OverviewProxyService.class); + overviewProxy.addCallback(new OverviewProxyService.OverviewProxyListener() { + @Override + public void onAssistantProgress(float progress) { + // Progress goes from 0 to 1 to indicate how close the assist gesture is to + // completion. + onInvocationProgress(INVOCATION_TYPE_GESTURE, progress); + } + + @Override + public void onAssistantGestureCompletion(float velocity) { + onGestureCompletion(velocity); + } + }); } protected void registerVoiceInteractionSessionListener() { @@ -196,21 +251,23 @@ public class AssistManager implements ConfigurationChangedReceiver { // Logs assistant start with invocation type. MetricsLogger.action( new LogMaker(MetricsEvent.ASSISTANT) - .setType(MetricsEvent.TYPE_OPEN).setSubtype(args.getInt(INVOCATION_TYPE_KEY))); + .setType(MetricsEvent.TYPE_OPEN).setSubtype( + args.getInt(INVOCATION_TYPE_KEY))); startAssistInternal(args, assistComponent, isService); } /** Called when the user is performing an assistant invocation action (e.g. Active Edge) */ public void onInvocationProgress(int type, float progress) { - // intentional no-op, vendor's AssistManager implementation should override if needed. + mUiController.onInvocationProgress(type, progress); } - /** Called when the user has invoked the assistant with the incoming velocity, in pixels per + /** + * Called when the user has invoked the assistant with the incoming velocity, in pixels per * millisecond. For invocations without a velocity (e.g. slow drag), the velocity is set to * zero. */ - public void onAssistantGestureCompletion(float velocity) { - // intentional no-op, vendor's AssistManager implementation should override if needed. + public void onGestureCompletion(float velocity) { + mUiController.onGestureCompletion(velocity); } public void hideAssist() { @@ -264,7 +321,7 @@ public class AssistManager implements ConfigurationChangedReceiver { Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0; final SearchManager searchManager = - (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); if (searchManager == null) { return; } @@ -329,7 +386,7 @@ public class AssistManager implements ConfigurationChangedReceiver { // Look for the search icon specified in the activity meta-data Bundle metaData = isService ? packageManager.getServiceInfo( - component, PackageManager.GET_META_DATA).metaData + component, PackageManager.GET_META_DATA).metaData : packageManager.getActivityInfo( component, PackageManager.GET_META_DATA).metaData; if (metaData != null) { diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java b/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java new file mode 100644 index 000000000000..162e09e4d23d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2019 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 com.android.systemui.assist.ui; + +import android.graphics.Path; + +/** + * Describes paths for circular rounded device corners. + */ +public final class CircularCornerPathRenderer extends CornerPathRenderer { + + private final int mCornerRadiusBottom; + private final int mCornerRadiusTop; + private final int mHeight; + private final int mWidth; + private final Path mPath = new Path(); + + public CircularCornerPathRenderer(int cornerRadiusBottom, int cornerRadiusTop, + int width, int height) { + mCornerRadiusBottom = cornerRadiusBottom; + mCornerRadiusTop = cornerRadiusTop; + mHeight = height; + mWidth = width; + } + + @Override // CornerPathRenderer + public Path getCornerPath(Corner corner) { + mPath.reset(); + switch (corner) { + case BOTTOM_LEFT: + mPath.moveTo(0, mHeight - mCornerRadiusBottom); + mPath.arcTo(0, mHeight - mCornerRadiusBottom * 2, mCornerRadiusBottom * 2, mHeight, + 180, -90, true); + break; + case BOTTOM_RIGHT: + mPath.moveTo(mWidth - mCornerRadiusBottom, mHeight); + mPath.arcTo(mWidth - mCornerRadiusBottom * 2, mHeight - mCornerRadiusBottom * 2, + mWidth, mHeight, 90, -90, true); + break; + case TOP_RIGHT: + mPath.moveTo(mWidth, mCornerRadiusTop); + mPath.arcTo(mWidth - mCornerRadiusTop, 0, mWidth, mCornerRadiusTop, 0, -90, true); + break; + case TOP_LEFT: + mPath.moveTo(mCornerRadiusTop, 0); + mPath.arcTo(0, 0, mCornerRadiusTop, mCornerRadiusTop, 270, -90, true); + break; + } + return mPath; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java b/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java new file mode 100644 index 000000000000..2b40e6501fdd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019 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 com.android.systemui.assist.ui; + +import android.graphics.Path; +import android.graphics.PointF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Handles paths along device corners. + */ +public abstract class CornerPathRenderer { + + // The maximum delta between the corner curve and points approximating the corner curve. + private static final float ACCEPTABLE_ERROR = 0.1f; + + /** + * For convenience, labels the four device corners. + * + * Corners must be listed in CCW order, otherwise we'll break rotation. + */ + public enum Corner { + BOTTOM_LEFT, + BOTTOM_RIGHT, + TOP_RIGHT, + TOP_LEFT + } + + /** + * Returns the path along the inside of a corner (centered insetAmountPx from the corner's + * edge). + */ + public Path getInsetPath(Corner corner, float insetAmountPx) { + return approximateInnerPath(getCornerPath(corner), -insetAmountPx); + } + + /** + * Returns the path of a corner (centered on the exact corner). Must be implemented by extending + * classes, based on the device-specific rounded corners. A default implementation for circular + * corners is provided by CircularCornerPathRenderer. + */ + public abstract Path getCornerPath(Corner corner); + + private Path approximateInnerPath(Path input, float delta) { + List<PointF> points = shiftBy(getApproximatePoints(input), delta); + return toPath(points); + } + + private ArrayList<PointF> getApproximatePoints(Path path) { + float[] rawInput = path.approximate(ACCEPTABLE_ERROR); + + ArrayList<PointF> output = new ArrayList<>(); + + for (int i = 0; i < rawInput.length; i = i + 3) { + output.add(new PointF(rawInput[i + 1], rawInput[i + 2])); + } + + return output; + } + + private ArrayList<PointF> shiftBy(ArrayList<PointF> input, float delta) { + ArrayList<PointF> output = new ArrayList<>(); + + for (int i = 0; i < input.size(); i++) { + PointF point = input.get(i); + PointF normal = normalAt(input, i); + PointF shifted = + new PointF(point.x + (normal.x * delta), point.y + (normal.y * delta)); + output.add(shifted); + } + return output; + } + + private Path toPath(List<PointF> points) { + Path path = new Path(); + if (points.size() > 0) { + path.moveTo(points.get(0).x, points.get(0).y); + for (PointF point : points.subList(1, points.size())) { + path.lineTo(point.x, point.y); + } + } + return path; + } + + private PointF normalAt(List<PointF> points, int index) { + PointF d1; + if (index == 0) { + d1 = new PointF(0, 0); + } else { + PointF point = points.get(index); + PointF previousPoint = points.get(index - 1); + d1 = new PointF((point.x - previousPoint.x), (point.y - previousPoint.y)); + } + + PointF d2; + if (index == (points.size() - 1)) { + d2 = new PointF(0, 0); + } else { + PointF point = points.get(index); + PointF nextPoint = points.get(index + 1); + d2 = new PointF((nextPoint.x - point.x), (nextPoint.y - point.y)); + } + + return rotate90Ccw(normalize(new PointF(d1.x + d2.x, d1.y + d2.y))); + } + + private PointF rotate90Ccw(PointF input) { + return new PointF(-input.y, input.x); + } + + private float magnitude(PointF point) { + return (float) Math.sqrt((point.x * point.x) + (point.y * point.y)); + } + + private PointF normalize(PointF point) { + float magnitude = magnitude(point); + if (magnitude == 0.f) { + return point; + } + + float normal = 1 / magnitude; + return new PointF((point.x * normal), (point.y * normal)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java new file mode 100644 index 000000000000..7ad6dfd2672b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2019 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 com.android.systemui.assist.ui; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.ColorInt; +import android.content.Context; +import android.graphics.PixelFormat; +import android.metrics.LogMaker; +import android.os.Bundle; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.WindowManager; +import android.view.animation.PathInterpolator; +import android.widget.FrameLayout; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; +import com.android.systemui.R; +import com.android.systemui.assist.AssistManager; + +/** + * Default UiController implementation. Shows white edge lights along the bottom of the phone, + * expanding from the corners to meet in the center. + */ +public class DefaultUiController implements AssistManager.UiController { + + private static final String TAG = "DefaultUiController"; + + private static final long ANIM_DURATION_MS = 200; + + protected final FrameLayout mRoot; + + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mLayoutParams; + private final PathInterpolator mProgressInterpolator = new PathInterpolator(.83f, 0, .84f, 1); + + private boolean mAttached = false; + private boolean mInvocationInProgress = false; + private float mLastInvocationProgress = 0; + + private ValueAnimator mInvocationAnimator = new ValueAnimator(); + private InvocationLightsView mInvocationLightsView; + + public DefaultUiController(Context context) { + mRoot = new FrameLayout(context); + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + mLayoutParams = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + mLayoutParams.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + mLayoutParams.gravity = Gravity.BOTTOM; + mLayoutParams.setTitle("Assist"); + + mInvocationLightsView = (InvocationLightsView) + LayoutInflater.from(context).inflate(R.layout.invocation_lights, mRoot, false); + mRoot.addView(mInvocationLightsView); + } + + @Override // AssistManager.UiController + public void processBundle(Bundle bundle) { + Log.e(TAG, "Bundle received but handling is not implemented; ignoring"); + } + + @Override // AssistManager.UiController + public void onInvocationProgress(int type, float progress) { + if (progress == 1) { + animateInvocationCompletion(type, 0); + } else if (progress == 0) { + mInvocationInProgress = false; + hide(); + } else { + if (!mInvocationInProgress) { + attach(); + mInvocationInProgress = true; + } + setProgressInternal(type, progress); + } + mLastInvocationProgress = progress; + + // Logs assistant invocation start. + if (!mInvocationInProgress && progress > 0.f) { + MetricsLogger.action(new LogMaker(MetricsEvent.ASSISTANT) + .setType(MetricsEvent.TYPE_ACTION)); + } + // Logs assistant invocation cancelled. + if (mInvocationInProgress && progress == 0f) { + MetricsLogger.action(new LogMaker(MetricsEvent.ASSISTANT) + .setType(MetricsEvent.TYPE_DISMISS).setSubtype(0)); + } + } + + @Override // AssistManager.UiController + public void onGestureCompletion(float velocity) { + animateInvocationCompletion(AssistManager.INVOCATION_TYPE_GESTURE, velocity); + } + + @Override // AssistManager.UiController + public void hide() { + Dependency.get(AssistManager.class).hideAssist(); + detach(); + if (mInvocationAnimator.isRunning()) { + mInvocationAnimator.cancel(); + } + mInvocationLightsView.hide(); + mInvocationInProgress = false; + } + + /** + * Sets the colors of the four invocation lights, from left to right. + */ + public void setInvocationColors(@ColorInt int color1, @ColorInt int color2, + @ColorInt int color3, @ColorInt int color4) { + mInvocationLightsView.setColors(color1, color2, color3, color4); + } + + private void attach() { + if (!mAttached) { + mWindowManager.addView(mRoot, mLayoutParams); + mAttached = true; + } + } + + private void detach() { + if (mAttached) { + mWindowManager.removeViewImmediate(mRoot); + mAttached = false; + } + } + + private void setProgressInternal(int type, float progress) { + mInvocationLightsView.onInvocationProgress( + mProgressInterpolator.getInterpolation(progress)); + } + + private void animateInvocationCompletion(int type, float velocity) { + mInvocationAnimator = ValueAnimator.ofFloat(mLastInvocationProgress, 1); + mInvocationAnimator.setStartDelay(1); + mInvocationAnimator.setDuration(ANIM_DURATION_MS); + mInvocationAnimator.addUpdateListener( + animation -> setProgressInternal(type, (float) animation.getAnimatedValue())); + mInvocationAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mInvocationInProgress = false; + mLastInvocationProgress = 0; + hide(); + } + }); + mInvocationAnimator.start(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java new file mode 100644 index 000000000000..251229f42da3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2019 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 com.android.systemui.assist.ui; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.Surface; + +/** + * Utility class for determining screen and corner dimensions. + */ +public class DisplayUtils { + /** + * Converts given distance from dp to pixels. + */ + public static int convertDpToPx(float dp, Context context) { + Display d = context.getDisplay(); + + DisplayMetrics dm = new DisplayMetrics(); + d.getRealMetrics(dm); + + return (int) Math.ceil(dp * dm.density); + } + + /** + * The width of the display. + * + * - Not affected by rotation. + * - Includes system decor. + */ + public static int getWidth(Context context) { + Display d = context.getDisplay(); + + DisplayMetrics dm = new DisplayMetrics(); + d.getRealMetrics(dm); + + int rotation = d.getRotation(); + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { + return dm.widthPixels; + } else { + return dm.heightPixels; + } + } + + /** + * The height of the display. + * + * - Not affected by rotation. + * - Includes system decor. + */ + public static int getHeight(Context context) { + Display d = context.getDisplay(); + + DisplayMetrics dm = new DisplayMetrics(); + d.getRealMetrics(dm); + + int rotation = d.getRotation(); + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { + return dm.heightPixels; + } else { + return dm.widthPixels; + } + } + + /** + * Returns the radius of the bottom corners (the distance from the true corner to the point + * where the curve ends), in pixels. + */ + public static int getCornerRadiusBottom(Context context) { + int radius = 0; + + int resourceId = context.getResources().getIdentifier("rounded_corner_radius_bottom", + "dimen", "android"); + if (resourceId > 0) { + radius = context.getResources().getDimensionPixelSize(resourceId); + } + + if (radius == 0) { + radius = getCornerRadiusDefault(context); + } + return radius; + } + + /** + * Returns the radius of the top corners (the distance from the true corner to the point where + * the curve ends), in pixels. + */ + public static int getCornerRadiusTop(Context context) { + int radius = 0; + + int resourceId = context.getResources().getIdentifier("rounded_corner_radius_top", + "dimen", "android"); + if (resourceId > 0) { + radius = context.getResources().getDimensionPixelSize(resourceId); + } + + if (radius == 0) { + radius = getCornerRadiusDefault(context); + } + return radius; + } + + private static int getCornerRadiusDefault(Context context) { + int radius = 0; + + int resourceId = context.getResources().getIdentifier("rounded_corner_radius", "dimen", + "android"); + if (resourceId > 0) { + radius = context.getResources().getDimensionPixelSize(resourceId); + } + return radius; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java b/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java new file mode 100644 index 000000000000..9ae02c5e3104 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2019 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 com.android.systemui.assist.ui; + +import androidx.annotation.ColorInt; + +/** + * Represents a line drawn on the perimeter of the display. + * + * Offsets and lengths are both normalized to the perimeter of the display – ex. a length of 1 + * is equal to the perimeter of the display. Positions move counter-clockwise as values increase. + * + * If there is no bottom corner radius, the origin is the bottom-left corner. + * If there is a bottom corner radius, the origin is immediately after the bottom corner radius, + * counter-clockwise. + */ +public final class EdgeLight { + @ColorInt + private int mColor; + private float mOffset; + private float mLength; + + /** Copies a list of EdgeLights. */ + public static EdgeLight[] copy(EdgeLight[] array) { + EdgeLight[] copy = new EdgeLight[array.length]; + for (int i = 0; i < array.length; i++) { + copy[i] = new EdgeLight(array[i]); + } + return copy; + } + + public EdgeLight(@ColorInt int color, float offset, float length) { + mColor = color; + mOffset = offset; + mLength = length; + } + + public EdgeLight(EdgeLight sourceLight) { + mColor = sourceLight.getColor(); + mOffset = sourceLight.getOffset(); + mLength = sourceLight.getLength(); + } + + /** Returns the current edge light color. */ + @ColorInt + public int getColor() { + return mColor; + } + + /** Sets the edge light color. */ + public void setColor(@ColorInt int color) { + mColor = color; + } + + /** Returns the edge light length, in units of the total device perimeter. */ + public float getLength() { + return mLength; + } + + /** Sets the edge light length, in units of the total device perimeter. */ + public void setLength(float length) { + mLength = length; + } + + /** + * Returns the current offset, in units of the total device perimeter and measured from the + * bottom-left corner (see class description). + */ + public float getOffset() { + return mOffset; + } + + /** + * Sets the current offset, in units of the total device perimeter and measured from the + * bottom-left corner (see class description). + */ + public void setOffset(float offset) { + mOffset = offset; + } + + /** Returns the center, measured from the bottom-left corner (see class description). */ + public float getCenter() { + return mOffset + (mLength / 2.f); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java b/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java new file mode 100644 index 000000000000..de1d7c8c0a04 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2019 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 com.android.systemui.assist.ui; + +import android.annotation.ColorInt; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.util.Log; +import android.util.MathUtils; +import android.view.View; + +import com.android.systemui.R; + +import java.util.ArrayList; + +/** + * Shows lights at the bottom of the phone, marking the invocation progress. + */ +public class InvocationLightsView extends View { + + private static final String TAG = "InvocationLightsView"; + + private static final int LIGHT_HEIGHT_DP = 3; + // minimum light length as a fraction of the corner length + private static final float MINIMUM_CORNER_RATIO = .6f; + + protected final ArrayList<EdgeLight> mAssistInvocationLights = new ArrayList<>(); + protected final PerimeterPathGuide mGuide; + + private final Paint mPaint = new Paint(); + // Path used to render lights. One instance is used to draw all lights and is cached to avoid + // allocation on each frame. + private final Path mPath = new Path(); + private final int mViewHeight; + + // Allocate variable for screen location lookup to avoid memory alloc onDraw() + private int[] mScreenLocation = new int[2]; + + public InvocationLightsView(Context context) { + this(context, null); + } + + public InvocationLightsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + int strokeWidth = DisplayUtils.convertDpToPx(LIGHT_HEIGHT_DP, context); + mPaint.setStrokeWidth(strokeWidth); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeJoin(Paint.Join.MITER); + mPaint.setAntiAlias(true); + + int cornerRadiusBottom = DisplayUtils.getCornerRadiusBottom(context); + int cornerRadiusTop = DisplayUtils.getCornerRadiusTop(context); + int displayWidth = DisplayUtils.getWidth(context); + int displayHeight = DisplayUtils.getHeight(context); + CircularCornerPathRenderer cornerPathRenderer = new CircularCornerPathRenderer( + cornerRadiusBottom, cornerRadiusTop, displayWidth, displayHeight); + mGuide = new PerimeterPathGuide(context, cornerPathRenderer, + strokeWidth / 2, displayWidth, displayHeight); + + mViewHeight = Math.max(cornerRadiusBottom, cornerRadiusTop); + + @ColorInt int lightColor = getResources().getColor(R.color.default_invocation_lights_color); + for (int i = 0; i < 4; i++) { + mAssistInvocationLights.add(new EdgeLight(lightColor, 0, 0)); + } + } + + /** + * Updates positions of the invocation lights based on the progress (a float between 0 and 1). + * The lights begin at the device corners and expand inward until they meet at the center. + */ + public void onInvocationProgress(float progress) { + if (progress == 0) { + setVisibility(View.GONE); + } else { + float cornerLengthNormalized = + mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM_LEFT); + float arcLengthNormalized = cornerLengthNormalized * MINIMUM_CORNER_RATIO; + float arcOffsetNormalized = (cornerLengthNormalized - arcLengthNormalized) / 2f; + + float minLightLength = arcLengthNormalized / 2; + float maxLightLength = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) / 4f; + + float lightLength = MathUtils.lerp(minLightLength, maxLightLength, progress); + + float leftStart = (-cornerLengthNormalized + arcOffsetNormalized) * (1 - progress); + float rightStart = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) + + (cornerLengthNormalized - arcOffsetNormalized) * (1 - progress); + + setLight(0, leftStart, lightLength); + setLight(1, leftStart + lightLength, lightLength); + setLight(2, rightStart - (lightLength * 2), lightLength); + setLight(3, rightStart - lightLength, lightLength); + setVisibility(View.VISIBLE); + } + invalidate(); + } + + /** + * Hides and resets the invocation lights. + */ + public void hide() { + setVisibility(GONE); + for (EdgeLight light : mAssistInvocationLights) { + light.setLength(0); + } + } + + /** + * Sets the invocation light colors, from left to right. + */ + public void setColors(@ColorInt int color1, @ColorInt int color2, + @ColorInt int color3, @ColorInt int color4) { + mAssistInvocationLights.get(0).setColor(color1); + mAssistInvocationLights.get(1).setColor(color2); + mAssistInvocationLights.get(2).setColor(color3); + mAssistInvocationLights.get(3).setColor(color4); + } + + @Override + protected void onFinishInflate() { + getLayoutParams().height = mViewHeight; + requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + int rotation = getContext().getDisplay().getRotation(); + mGuide.setRotation(rotation); + } + + @Override + protected void onDraw(Canvas canvas) { + // If the view doesn't take up the whole screen, offset the canvas by its translation + // distance such that PerimeterPathGuide's paths are drawn properly based upon the actual + // screen edges. + getLocationOnScreen(mScreenLocation); + canvas.translate(-mScreenLocation[0], -mScreenLocation[1]); + + // if the lights are different colors, the inner ones need to be drawn last and with a + // square cap so that the join between lights is straight + mPaint.setStrokeCap(Paint.Cap.ROUND); + renderLight(mAssistInvocationLights.get(0), canvas); + renderLight(mAssistInvocationLights.get(3), canvas); + + mPaint.setStrokeCap(Paint.Cap.SQUARE); + renderLight(mAssistInvocationLights.get(1), canvas); + renderLight(mAssistInvocationLights.get(2), canvas); + } + + protected void setLight(int index, float offset, float length) { + if (index < 0 || index >= 4) { + Log.w(TAG, "invalid invocation light index: " + index); + } + mAssistInvocationLights.get(index).setOffset(offset); + mAssistInvocationLights.get(index).setLength(length); + } + + private void renderLight(EdgeLight light, Canvas canvas) { + mGuide.strokeSegment(mPath, light.getOffset(), light.getOffset() + light.getLength()); + mPaint.setColor(light.getColor()); + canvas.drawPath(mPath, mPaint); + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java b/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java new file mode 100644 index 000000000000..8eea36892aa7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2019 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 com.android.systemui.assist.ui; + +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.util.Log; +import android.util.Pair; +import android.view.Surface; + +import androidx.core.math.MathUtils; + +/** + * PerimeterPathGuide establishes a coordinate system for drawing paths along the perimeter of the + * screen. All positions around the perimeter have a coordinate [0, 1). The origin is the bottom + * left corner of the screen, to the right of the curved corner, if any. Coordinates increase + * counter-clockwise around the screen. + * + * Non-square screens require PerimeterPathGuide to be notified when the rotation changes, such that + * it can recompute the edge lengths for the coordinate system. + */ +public class PerimeterPathGuide { + + private static final String TAG = "PerimeterPathGuide"; + + /** + * For convenience, labels sections of the device perimeter. + * + * Must be listed in CCW order. + */ + public enum Region { + BOTTOM, + BOTTOM_RIGHT, + RIGHT, + TOP_RIGHT, + TOP, + TOP_LEFT, + LEFT, + BOTTOM_LEFT + } + + private final int mDeviceWidthPx; + private final int mDeviceHeightPx; + private final int mTopCornerRadiusPx; + private final int mBottomCornerRadiusPx; + + private class RegionAttributes { + public float absoluteLength; + public float normalizedLength; + public float endCoordinate; + public Path path; + } + + // Allocate a Path and PathMeasure for use by intermediate operations that would otherwise have + // to allocate. reset() must be called before using this path, this ensures state from previous + // operations is cleared. + private final Path mScratchPath = new Path(); + private final CornerPathRenderer mCornerPathRenderer; + private final PathMeasure mScratchPathMeasure = new PathMeasure(mScratchPath, false); + private RegionAttributes[] mRegions; + private final int mEdgeInset; + private int mRotation = ROTATION_0; + + public PerimeterPathGuide(Context context, CornerPathRenderer cornerPathRenderer, + int edgeInset, int screenWidth, int screenHeight) { + mCornerPathRenderer = cornerPathRenderer; + mDeviceWidthPx = screenWidth; + mDeviceHeightPx = screenHeight; + mTopCornerRadiusPx = DisplayUtils.getCornerRadiusTop(context); + mBottomCornerRadiusPx = DisplayUtils.getCornerRadiusBottom(context); + mEdgeInset = edgeInset; + + mRegions = new RegionAttributes[8]; + for (int i = 0; i < mRegions.length; i++) { + mRegions[i] = new RegionAttributes(); + } + computeRegions(); + } + + /** + * Sets the rotation. + * + * @param rotation one of Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, + * Surface.ROTATION_270 + */ + public void setRotation(int rotation) { + if (rotation != mRotation) { + switch (rotation) { + case ROTATION_0: + case ROTATION_90: + case ROTATION_180: + case ROTATION_270: + mRotation = rotation; + computeRegions(); + break; + default: + Log.e(TAG, "Invalid rotation provided: " + rotation); + } + } + } + + /** + * Sets path to the section of the perimeter between startCoord and endCoord (measured + * counter-clockwise from the bottom left). + */ + public void strokeSegment(Path path, float startCoord, float endCoord) { + path.reset(); + + startCoord = ((startCoord % 1) + 1) % 1; // Wrap to the range [0, 1). + endCoord = ((endCoord % 1) + 1) % 1; // Wrap to the range [0, 1). + boolean outOfOrder = startCoord > endCoord; + + if (outOfOrder) { + strokeSegmentInternal(path, startCoord, 1f); + startCoord = 0; + } + strokeSegmentInternal(path, startCoord, endCoord); + } + + /** + * Returns the device perimeter in pixels. + */ + public float getPerimeterPx() { + float total = 0; + for (RegionAttributes region : mRegions) { + total += region.absoluteLength; + } + return total; + } + + /** + * Returns the bottom corner radius in pixels. + */ + public float getBottomCornerRadiusPx() { + return mBottomCornerRadiusPx; + } + + /** + * Given a region and a progress value [0,1] indicating the counter-clockwise progress within + * that region, compute the global [0,1) coordinate. + */ + public float getCoord(Region region, float progress) { + RegionAttributes regionAttributes = mRegions[region.ordinal()]; + progress = MathUtils.clamp(progress, 0, 1); + return regionAttributes.endCoordinate - (1 - progress) * regionAttributes.normalizedLength; + } + + /** + * Returns the center of the provided region, relative to the entire perimeter. + */ + public float getRegionCenter(Region region) { + return getCoord(region, 0.5f); + } + + /** + * Returns the width of the provided region, in units relative to the entire perimeter. + */ + public float getRegionWidth(Region region) { + return mRegions[region.ordinal()].normalizedLength; + } + + /** + * Points are expressed in terms of their relative position on the perimeter of the display, + * moving counter-clockwise. This method converts a point to clockwise, assisting use cases + * such as animating to a point clockwise instead of counter-clockwise. + * + * @param point A point in the range from 0 to 1. + * @return A point in the range of -1 to 0 that represents the same location as {@code point}. + */ + public static float makeClockwise(float point) { + return point - 1; + } + + private int getPhysicalCornerRadius(CircularCornerPathRenderer.Corner corner) { + if (corner == CircularCornerPathRenderer.Corner.BOTTOM_LEFT + || corner == CircularCornerPathRenderer.Corner.BOTTOM_RIGHT) { + return mBottomCornerRadiusPx; + } + return mTopCornerRadiusPx; + } + + // Populate mRegions based upon the current rotation value. + private void computeRegions() { + int screenWidth = mDeviceWidthPx; + int screenHeight = mDeviceHeightPx; + + int rotateMatrix = 0; + + switch (mRotation) { + case ROTATION_90: + rotateMatrix = -90; + break; + case ROTATION_180: + rotateMatrix = -180; + break; + case Surface.ROTATION_270: + rotateMatrix = -270; + break; + } + + Matrix matrix = new Matrix(); + matrix.postRotate(rotateMatrix, mDeviceWidthPx / 2, mDeviceHeightPx / 2); + + if (mRotation == ROTATION_90 || mRotation == Surface.ROTATION_270) { + screenHeight = mDeviceWidthPx; + screenWidth = mDeviceHeightPx; + matrix.postTranslate((mDeviceHeightPx + - mDeviceWidthPx) / 2, (mDeviceWidthPx - mDeviceHeightPx) / 2); + } + + CircularCornerPathRenderer.Corner screenBottomLeft = getRotatedCorner( + CircularCornerPathRenderer.Corner.BOTTOM_LEFT); + CircularCornerPathRenderer.Corner screenBottomRight = getRotatedCorner( + CircularCornerPathRenderer.Corner.BOTTOM_RIGHT); + CircularCornerPathRenderer.Corner screenTopLeft = getRotatedCorner( + CircularCornerPathRenderer.Corner.TOP_LEFT); + CircularCornerPathRenderer.Corner screenTopRight = getRotatedCorner( + CircularCornerPathRenderer.Corner.TOP_RIGHT); + + mRegions[Region.BOTTOM_LEFT.ordinal()].path = + mCornerPathRenderer.getInsetPath(screenBottomLeft, mEdgeInset); + mRegions[Region.BOTTOM_RIGHT.ordinal()].path = + mCornerPathRenderer.getInsetPath(screenBottomRight, mEdgeInset); + mRegions[Region.TOP_RIGHT.ordinal()].path = + mCornerPathRenderer.getInsetPath(screenTopRight, mEdgeInset); + mRegions[Region.TOP_LEFT.ordinal()].path = + mCornerPathRenderer.getInsetPath(screenTopLeft, mEdgeInset); + + mRegions[Region.BOTTOM_LEFT.ordinal()].path.transform(matrix); + mRegions[Region.BOTTOM_RIGHT.ordinal()].path.transform(matrix); + mRegions[Region.TOP_RIGHT.ordinal()].path.transform(matrix); + mRegions[Region.TOP_LEFT.ordinal()].path.transform(matrix); + + + Path bottomPath = new Path(); + bottomPath.moveTo(getPhysicalCornerRadius(screenBottomLeft), screenHeight - mEdgeInset); + bottomPath.lineTo(screenWidth - getPhysicalCornerRadius(screenBottomRight), + screenHeight - mEdgeInset); + mRegions[Region.BOTTOM.ordinal()].path = bottomPath; + + Path topPath = new Path(); + topPath.moveTo(screenWidth - getPhysicalCornerRadius(screenTopRight), mEdgeInset); + topPath.lineTo(getPhysicalCornerRadius(screenTopLeft), mEdgeInset); + mRegions[Region.TOP.ordinal()].path = topPath; + + Path rightPath = new Path(); + rightPath.moveTo(screenWidth - mEdgeInset, + screenHeight - getPhysicalCornerRadius(screenBottomRight)); + rightPath.lineTo(screenWidth - mEdgeInset, getPhysicalCornerRadius(screenTopRight)); + mRegions[Region.RIGHT.ordinal()].path = rightPath; + + Path leftPath = new Path(); + leftPath.moveTo(mEdgeInset, + getPhysicalCornerRadius(screenTopLeft)); + leftPath.lineTo(mEdgeInset, screenHeight - getPhysicalCornerRadius(screenBottomLeft)); + mRegions[Region.LEFT.ordinal()].path = leftPath; + + float perimeterLength = 0; + PathMeasure pathMeasure = new PathMeasure(); + for (int i = 0; i < mRegions.length; i++) { + pathMeasure.setPath(mRegions[i].path, false); + mRegions[i].absoluteLength = pathMeasure.getLength(); + perimeterLength += mRegions[i].absoluteLength; + } + + float accum = 0; + for (int i = 0; i < mRegions.length; i++) { + mRegions[i].normalizedLength = mRegions[i].absoluteLength / perimeterLength; + accum += mRegions[i].normalizedLength; + mRegions[i].endCoordinate = accum; + } + } + + private CircularCornerPathRenderer.Corner getRotatedCorner( + CircularCornerPathRenderer.Corner screenCorner) { + int corner = screenCorner.ordinal(); + switch (mRotation) { + case ROTATION_90: + corner += 3; + break; + case ROTATION_180: + corner += 2; + break; + case Surface.ROTATION_270: + corner += 1; + break; + } + return CircularCornerPathRenderer.Corner.values()[corner % 4]; + } + + private void strokeSegmentInternal(Path path, float startCoord, float endCoord) { + Pair<Region, Float> startPoint = placePoint(startCoord); + Pair<Region, Float> endPoint = placePoint(endCoord); + + if (startPoint.first.equals(endPoint.first)) { + strokeRegion(path, startPoint.first, startPoint.second, endPoint.second); + } else { + strokeRegion(path, startPoint.first, startPoint.second, 1f); + boolean hitStart = false; + for (Region r : Region.values()) { + if (r.equals(startPoint.first)) { + hitStart = true; + continue; + } + if (hitStart) { + if (!r.equals(endPoint.first)) { + strokeRegion(path, r, 0f, 1f); + } else { + strokeRegion(path, r, 0f, endPoint.second); + break; + } + } + } + } + } + + private void strokeRegion(Path path, Region r, float relativeStart, float relativeEnd) { + if (relativeStart == relativeEnd) { + return; + } + + mScratchPathMeasure.setPath(mRegions[r.ordinal()].path, false); + mScratchPathMeasure.getSegment(relativeStart * mScratchPathMeasure.getLength(), + relativeEnd * mScratchPathMeasure.getLength(), path, true); + } + + /** + * Return the Region where the point is located, and its relative position within that region + * (from 0 to 1). + * Note that we move counterclockwise around the perimeter; for example, a relative position of + * 0 in + * the BOTTOM region is on the left side of the screen, but in the TOP region it’s on the + * right. + */ + private Pair<Region, Float> placePoint(float coord) { + if (0 > coord || coord > 1) { + coord = ((coord % 1) + 1) + % 1; // Wrap to the range [0, 1). Inputs of exactly 1 are preserved. + } + + Region r = getRegionForPoint(coord); + if (r.equals(Region.BOTTOM)) { + return Pair.create(r, coord / mRegions[r.ordinal()].normalizedLength); + } else { + float coordOffsetInRegion = coord - mRegions[r.ordinal() - 1].endCoordinate; + float coordRelativeToRegion = + coordOffsetInRegion / mRegions[r.ordinal()].normalizedLength; + return Pair.create(r, coordRelativeToRegion); + } + } + + private Region getRegionForPoint(float coord) { + // If coord is outside of [0,1], wrap to [0,1). + if (coord < 0 || coord > 1) { + coord = ((coord % 1) + 1) % 1; + } + + for (Region region : Region.values()) { + if (coord <= mRegions[region.ordinal()].endCoordinate) { + return region; + } + } + + // Should never happen. + Log.e(TAG, "Fell out of getRegionForPoint"); + return Region.BOTTOM; + } +} |