Merge "Add white edgelights to AOSP" into qt-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
0bcd698481
23
packages/SystemUI/res/layout/invocation_lights.xml
Normal file
23
packages/SystemUI/res/layout/invocation_lights.xml
Normal 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"/>
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user