Merge "Add white edgelights to AOSP" into qt-dev

This commit is contained in:
Miranda Kephart
2019-05-30 02:15:55 +00:00
committed by Android (Google) Code Review
10 changed files with 1295 additions and 19 deletions

View File

@@ -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"/>

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 its 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;
}
}