summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/layout/invocation_lights.xml23
-rw-r--r--packages/SystemUI/res/values/colors.xml9
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/AssistManager.java89
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java65
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java140
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java178
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java128
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java99
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java194
-rw-r--r--packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java389
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;
+ }
+}