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>
|
<color name="smart_reply_button_stroke">#ffdadce0</color>
|
||||||
|
|
||||||
<!-- Biometric dialog colors -->
|
<!-- 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_gray">#ff757575</color>
|
||||||
<color name="biometric_dialog_accent">#ff008577</color> <!-- dark teal -->
|
<color name="biometric_dialog_accent">#ff008577</color> <!-- dark teal -->
|
||||||
<color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 -->
|
<color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 -->
|
||||||
|
|
||||||
<!-- Logout button -->
|
<!-- Logout button -->
|
||||||
<color name="logout_button_bg_color">#ccffffff</color>
|
<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 -->
|
<!-- GM2 colors -->
|
||||||
<color name="GM2_grey_50">#F8F9FA</color>
|
<color name="GM2_grey_50">#F8F9FA</color>
|
||||||
<color name="GM2_grey_100">#F1F3F4</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.keyguard.KeyguardUpdateMonitor;
|
||||||
import com.android.settingslib.applications.InterestingConfigChanges;
|
import com.android.settingslib.applications.InterestingConfigChanges;
|
||||||
import com.android.systemui.ConfigurationChangedReceiver;
|
import com.android.systemui.ConfigurationChangedReceiver;
|
||||||
|
import com.android.systemui.Dependency;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.SysUiServiceProvider;
|
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.CommandQueue;
|
||||||
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
|
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
|
||||||
|
|
||||||
@@ -50,6 +53,40 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController;
|
|||||||
*/
|
*/
|
||||||
public class AssistManager implements ConfigurationChangedReceiver {
|
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";
|
private static final String TAG = "AssistManager";
|
||||||
|
|
||||||
// Note that VERBOSE logging may leak PII (e.g. transcription contents).
|
// 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 InterestingConfigChanges mInterestingConfigChanges;
|
||||||
private final PhoneStateMonitor mPhoneStateMonitor;
|
private final PhoneStateMonitor mPhoneStateMonitor;
|
||||||
private final AssistHandleBehaviorController mHandleController;
|
private final AssistHandleBehaviorController mHandleController;
|
||||||
|
private final UiController mUiController;
|
||||||
|
|
||||||
private AssistOrbContainer mView;
|
private AssistOrbContainer mView;
|
||||||
private final DeviceProvisionedController mDeviceProvisionedController;
|
private final DeviceProvisionedController mDeviceProvisionedController;
|
||||||
@@ -85,16 +123,16 @@ public class AssistManager implements ConfigurationChangedReceiver {
|
|||||||
private IVoiceInteractionSessionShowCallback mShowCallback =
|
private IVoiceInteractionSessionShowCallback mShowCallback =
|
||||||
new IVoiceInteractionSessionShowCallback.Stub() {
|
new IVoiceInteractionSessionShowCallback.Stub() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailed() throws RemoteException {
|
public void onFailed() throws RemoteException {
|
||||||
mView.post(mHideRunnable);
|
mView.post(mHideRunnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onShown() throws RemoteException {
|
public void onShown() throws RemoteException {
|
||||||
mView.post(mHideRunnable);
|
mView.post(mHideRunnable);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private Runnable mHideRunnable = new Runnable() {
|
private Runnable mHideRunnable = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -119,6 +157,23 @@ public class AssistManager implements ConfigurationChangedReceiver {
|
|||||||
| ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS);
|
| ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS);
|
||||||
onConfigurationChanged(context.getResources().getConfiguration());
|
onConfigurationChanged(context.getResources().getConfiguration());
|
||||||
mShouldEnableOrb = !ActivityManager.isLowRamDeviceStatic();
|
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() {
|
protected void registerVoiceInteractionSessionListener() {
|
||||||
@@ -196,21 +251,23 @@ public class AssistManager implements ConfigurationChangedReceiver {
|
|||||||
// Logs assistant start with invocation type.
|
// Logs assistant start with invocation type.
|
||||||
MetricsLogger.action(
|
MetricsLogger.action(
|
||||||
new LogMaker(MetricsEvent.ASSISTANT)
|
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);
|
startAssistInternal(args, assistComponent, isService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called when the user is performing an assistant invocation action (e.g. Active Edge) */
|
/** Called when the user is performing an assistant invocation action (e.g. Active Edge) */
|
||||||
public void onInvocationProgress(int type, float progress) {
|
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
|
* millisecond. For invocations without a velocity (e.g. slow drag), the velocity is set to
|
||||||
* zero.
|
* zero.
|
||||||
*/
|
*/
|
||||||
public void onAssistantGestureCompletion(float velocity) {
|
public void onGestureCompletion(float velocity) {
|
||||||
// intentional no-op, vendor's AssistManager implementation should override if needed.
|
mUiController.onGestureCompletion(velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideAssist() {
|
public void hideAssist() {
|
||||||
@@ -264,7 +321,7 @@ public class AssistManager implements ConfigurationChangedReceiver {
|
|||||||
Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
|
Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
|
||||||
|
|
||||||
final SearchManager searchManager =
|
final SearchManager searchManager =
|
||||||
(SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
|
(SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
|
||||||
if (searchManager == null) {
|
if (searchManager == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -329,7 +386,7 @@ public class AssistManager implements ConfigurationChangedReceiver {
|
|||||||
// Look for the search icon specified in the activity meta-data
|
// Look for the search icon specified in the activity meta-data
|
||||||
Bundle metaData = isService
|
Bundle metaData = isService
|
||||||
? packageManager.getServiceInfo(
|
? packageManager.getServiceInfo(
|
||||||
component, PackageManager.GET_META_DATA).metaData
|
component, PackageManager.GET_META_DATA).metaData
|
||||||
: packageManager.getActivityInfo(
|
: packageManager.getActivityInfo(
|
||||||
component, PackageManager.GET_META_DATA).metaData;
|
component, PackageManager.GET_META_DATA).metaData;
|
||||||
if (metaData != null) {
|
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