Merge "Merge 25Q1 (ab/12770256) to aosp-main-future" into aosp-main-future
diff --git a/car-qc-lib/Android.bp b/car-qc-lib/Android.bp
index 4d3a297..76c8991 100644
--- a/car-qc-lib/Android.bp
+++ b/car-qc-lib/Android.bp
@@ -17,9 +17,22 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+genrule {
+ name: "statslog-carqclib-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module carqclib" +
+ " --javaPackage com.android.car.qc --javaClass CarQcLibStatsLog",
+ out: ["com/android/car/qc/CarQcLibStatsLog.java"],
+}
+
+carqclib_srcs = [
+ "src/**/*.java",
+ ":statslog-carqclib-java-gen",
+]
+
android_library {
name: "car-qc-lib",
- srcs: ["src/**/*.java"],
+ srcs: carqclib_srcs,
optimize: {
enabled: false,
},
diff --git a/car-qc-lib/src/com/android/car/qc/QCItem.java b/car-qc-lib/src/com/android/car/qc/QCItem.java
index c6826ae..b2bab66 100644
--- a/car-qc-lib/src/com/android/car/qc/QCItem.java
+++ b/car-qc-lib/src/com/android/car/qc/QCItem.java
@@ -29,6 +29,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
/**
* Base class for all quick controls elements.
@@ -61,6 +63,8 @@
private final boolean mIsClickableWhileDisabled;
private ActionHandler mActionHandler;
private ActionHandler mDisabledClickActionHandler;
+ private int mPackageUid;
+ private String mTag = "";
public QCItem(@NonNull @QCItemType String type) {
this(type, /* isEnabled= */true, /* isClickableWhileDisabled= */ false);
@@ -77,6 +81,8 @@
mType = in.readString();
mIsEnabled = in.readBoolean();
mIsClickableWhileDisabled = in.readBoolean();
+ mPackageUid = in.readInt();
+ mTag = in.readString();
}
@NonNull
@@ -93,6 +99,14 @@
return mIsClickableWhileDisabled;
}
+ public int getPackageUid() {
+ return mPackageUid;
+ }
+
+ public String getTag() {
+ return mTag;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -103,6 +117,8 @@
dest.writeString(mType);
dest.writeBoolean(mIsEnabled);
dest.writeBoolean(mIsClickableWhileDisabled);
+ dest.writeInt(mPackageUid);
+ dest.writeString(mTag);
}
public void setActionHandler(@Nullable ActionHandler handler) {
@@ -113,6 +129,22 @@
mDisabledClickActionHandler = handler;
}
+ public void setPackageUid(int packageUid) {
+ mPackageUid = packageUid;
+ }
+
+ public void setTag(String tag) {
+ //for privacy concerns, these tags should be hashed before they are
+ //recorded for metrics
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(tag.getBytes());
+ mTag = new String(hash);
+ } catch (NoSuchAlgorithmException e) {
+ mTag = "";
+ }
+ }
+
@Nullable
public ActionHandler getActionHandler() {
return mActionHandler;
diff --git a/car-qc-lib/src/com/android/car/qc/QCSlider.java b/car-qc-lib/src/com/android/car/qc/QCSlider.java
index 612274b..30a192d 100644
--- a/car-qc-lib/src/com/android/car/qc/QCSlider.java
+++ b/car-qc-lib/src/com/android/car/qc/QCSlider.java
@@ -21,7 +21,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
/**
* Quick Control Slider included in {@link QCRow}
*/
@@ -186,4 +185,8 @@
mInputAction, mDisabledClickAction);
}
}
+
+ int getSliderValueInPercentage(int value) {
+ return (int) (value * 1.0 / (getMax() - getMin()) * 100);
+ }
}
diff --git a/car-qc-lib/src/com/android/car/qc/StatsLogHelper.java b/car-qc-lib/src/com/android/car/qc/StatsLogHelper.java
new file mode 100644
index 0000000..e7cb5c5
--- /dev/null
+++ b/car-qc-lib/src/com/android/car/qc/StatsLogHelper.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 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.car.qc;
+
+import static com.android.car.qc.QCItem.QC_ACTION_SLIDER_VALUE;
+import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
+import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
+import static com.android.car.qc.QCItem.QC_TYPE_ACTION_TOGGLE;
+import static com.android.car.qc.QCItem.QC_TYPE_ROW;
+import static com.android.car.qc.QCItem.QC_TYPE_SLIDER;
+import static com.android.car.qc.QCItem.QC_TYPE_TILE;
+
+import android.annotation.IntDef;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+public class StatsLogHelper {
+ private static final String TAG = StatsLogHelper.class.getSimpleName();
+ private static StatsLogHelper sInstance;
+ private static final int DEFAULT_VALUE = -1;
+ private static final boolean DEFAULT_STATE = false;
+
+
+ /**
+ * IntDef representing enum values of CarQcLibEventReported.element_type.
+ */
+ @IntDef({
+ QcElementType.UNSPECIFIED_ELEMENT_TYPE,
+ QcElementType.QC_TYPE_LIST,
+ QcElementType.QC_TYPE_ROW,
+ QcElementType.QC_TYPE_TILE,
+ QcElementType.QC_TYPE_SLIDER,
+ QcElementType.QC_TYPE_ACTION_SWITCH,
+ QcElementType.QC_TYPE_ACTION_TOGGLE,
+ })
+
+ public @interface QcElementType {
+ int UNSPECIFIED_ELEMENT_TYPE =
+ CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__UNSPECIFIED_ELEMENT_TYPE;
+ int QC_TYPE_LIST =
+ CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_LIST;
+ int QC_TYPE_ROW =
+ CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_ROW;
+ int QC_TYPE_TILE =
+ CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_TILE;
+ int QC_TYPE_SLIDER =
+ CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_SLIDER;
+ int QC_TYPE_ACTION_SWITCH =
+ CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_ACTION_SWITCH;
+ int QC_TYPE_ACTION_TOGGLE =
+ CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_ACTION_TOGGLE;
+ }
+
+ /**
+ * Returns the current logging instance of StatsLogHelper to write this devices'
+ * CarQcLibStatsModule.
+ *
+ * @return the logging instance of StatsLogHelper.
+ */
+ public static StatsLogHelper getInstance() {
+ if (sInstance == null) {
+ sInstance = new StatsLogHelper();
+ }
+ return sInstance;
+ }
+
+ /**
+ * Writes to CarQcLibEvent atom with all the optional fields filled.
+ *
+ * @param qcHashedTag the tag of the QC
+ * @param qcElementType one of {@link QcElementType}
+ * @param qcValue the current value of the QC element
+ * @param qcState the current state of the QC element
+ */
+ private void writeCarQcLibEventReported(int packageUid, String qcHashedTag, int qcElementType,
+ int qcValue, boolean qcState) {
+ if (Build.isDebuggable()) {
+ Log.v(TAG, "writing CAR_QC_LIB_EVENT_REPORTED. packageUid=" + packageUid
+ + ", qcHashedTag=" + qcHashedTag + ", qcElementType= " + qcElementType
+ + ", qcValue=" + qcValue + ", qcState=" + qcState);
+ }
+ CarQcLibStatsLog.write(
+ /* atomId */ CarQcLibStatsLog.CAR_QC_LIB_EVENT_REPORTED,
+ /* packageUid */ packageUid,
+ /* qcHashedTag */ qcHashedTag,
+ /* qcElementType */ qcElementType,
+ /* qcValue */ qcValue,
+ /* qcState */ qcState);
+ }
+
+ /**
+ * Logs that there is an interaction on QC elements
+ */
+ public void logMetrics(QCItem item, Intent intent) {
+ // if we can't find package uid or tag, we don't need any metrics
+ if (item.getPackageUid() == 0 || item.getTag().isEmpty()) {
+ return;
+ }
+ int value = DEFAULT_VALUE;
+ boolean state = DEFAULT_STATE;
+ if (intent != null) {
+ Bundle bundle = intent.getExtras();
+ if (bundle != null) {
+ String type = item.getType();
+ if (type.equals(QC_TYPE_ACTION_SWITCH)
+ || type.equals(QC_TYPE_TILE)
+ || type.equals(QC_TYPE_ACTION_TOGGLE)) {
+ state = bundle.getBoolean(QC_ACTION_TOGGLE_STATE);
+ } else if (item.getType().equals(QC_TYPE_SLIDER)) {
+ int i = bundle.getInt(QC_ACTION_SLIDER_VALUE);
+ value = ((QCSlider) item).getSliderValueInPercentage(i);
+ }
+ }
+ writeCarQcLibEventReported(item.getPackageUid(), item.getTag(),
+ convertStringToIntQcType(item.getType()),
+ /* value= */ value, /* state= */ state);
+ return;
+ }
+ if (item.getType().equals(QC_TYPE_ROW) || item.getType().equals(QC_TYPE_TILE)) {
+ if (item.getDisabledClickAction() != null || item.getPrimaryAction() != null) {
+ writeCarQcLibEventReported(item.getPackageUid(), item.getTag(),
+ convertStringToIntQcType(item.getType()),
+ /* value= */ DEFAULT_VALUE, /* state= */ DEFAULT_STATE);
+ }
+ }
+ }
+
+ private int convertStringToIntQcType(String qcType) {
+ switch (qcType) {
+ case QCItem.QC_TYPE_LIST -> {
+ return CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_LIST;
+ }
+ case QC_TYPE_ROW -> {
+ return CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_ROW;
+ }
+ case QCItem.QC_TYPE_TILE -> {
+ return CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_TILE;
+ }
+ case QCItem.QC_TYPE_SLIDER -> {
+ return CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_SLIDER;
+ }
+ case QCItem.QC_TYPE_ACTION_SWITCH -> {
+ return CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_ACTION_SWITCH;
+ }
+ case QCItem.QC_TYPE_ACTION_TOGGLE -> {
+ return CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__QC_TYPE_ACTION_TOGGLE;
+ }
+ default -> {
+ return CarQcLibStatsLog
+ .CAR_QC_LIB_EVENT_REPORTED__ELEMENT_TYPE__UNSPECIFIED_ELEMENT_TYPE;
+ }
+ }
+ }
+}
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCRowView.java b/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
index acbdc70..efbb7f5 100644
--- a/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
+++ b/car-qc-lib/src/com/android/car/qc/view/QCRowView.java
@@ -21,6 +21,7 @@
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
import static com.android.car.qc.view.QCView.QCActionListener;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -54,6 +55,7 @@
import com.android.car.qc.QCRow;
import com.android.car.qc.QCSlider;
import com.android.car.qc.R;
+import com.android.car.qc.StatsLogHelper;
import com.android.car.ui.utils.CarUiUtils;
import com.android.car.ui.utils.DirectManipulationHelper;
import com.android.car.ui.uxr.DrawableStateToggleButton;
@@ -464,10 +466,18 @@
if (!item.isEnabled()) {
if (item.getDisabledClickAction() != null) {
try {
- item.getDisabledClickAction().send(getContext(), 0, intent);
+ item.getDisabledClickAction().send(getContext(), 0, intent,
+ /* requestCode= */ null,
+ /* fillInIntent= */ null,
+ /* options= */ null,
+ ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ .toBundle());
if (mActionListener != null) {
mActionListener.onQCAction(item, item.getDisabledClickAction());
}
+ StatsLogHelper.getInstance().logMetrics(item, intent);
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "Error sending intent", e);
}
@@ -476,16 +486,25 @@
if (mActionListener != null) {
mActionListener.onQCAction(item, item.getDisabledClickActionHandler());
}
+ StatsLogHelper.getInstance().logMetrics(item, intent);
}
return;
}
if (item.getPrimaryAction() != null) {
try {
- item.getPrimaryAction().send(getContext(), 0, intent);
+ item.getPrimaryAction().send(getContext(), 0, intent,
+ /* requestCode= */ null,
+ /* fillInIntent= */ null,
+ /* options= */ null,
+ ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ .toBundle());
if (mActionListener != null) {
mActionListener.onQCAction(item, item.getPrimaryAction());
}
+ StatsLogHelper.getInstance().logMetrics(item, intent);
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "Error sending intent", e);
}
@@ -494,6 +513,7 @@
if (mActionListener != null) {
mActionListener.onQCAction(item, item.getActionHandler());
}
+ StatsLogHelper.getInstance().logMetrics(item, intent);
}
}
diff --git a/car-qc-lib/src/com/android/car/qc/view/QCTileView.java b/car-qc-lib/src/com/android/car/qc/view/QCTileView.java
index 4173e25..4cf50bd 100644
--- a/car-qc-lib/src/com/android/car/qc/view/QCTileView.java
+++ b/car-qc-lib/src/com/android/car/qc/view/QCTileView.java
@@ -19,6 +19,7 @@
import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
import static com.android.car.qc.view.QCView.QCActionListener;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -34,6 +35,7 @@
import com.android.car.qc.QCItem;
import com.android.car.qc.QCTile;
import com.android.car.qc.R;
+import com.android.car.qc.StatsLogHelper;
import com.android.car.ui.utils.CarUiUtils;
import com.android.car.ui.uxr.DrawableStateToggleButton;
@@ -107,19 +109,30 @@
if (!qcTile.isEnabled()) {
if (qcTile.getDisabledClickAction() != null) {
try {
- qcTile.getDisabledClickAction().send(getContext(), 0, new Intent());
+ Intent intent = new Intent();
+ qcTile.getDisabledClickAction().send(getContext(), 0, intent,
+ /* requestCode= */ null,
+ /* fillInIntent= */ null,
+ /* options= */ null,
+ ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ .toBundle());
if (mActionListener != null) {
mActionListener.onQCAction(qcTile, qcTile.getDisabledClickAction());
}
+ StatsLogHelper.getInstance().logMetrics(qcTile, intent);
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "Error sending intent", e);
}
} else if (qcTile.getDisabledClickActionHandler() != null) {
+ Intent intent = new Intent();
qcTile.getDisabledClickActionHandler().onAction(qcTile, getContext(),
- new Intent());
+ intent);
if (mActionListener != null) {
mActionListener.onQCAction(qcTile, qcTile.getDisabledClickActionHandler());
}
+ StatsLogHelper.getInstance().logMetrics(qcTile, intent);
}
return;
}
@@ -133,10 +146,18 @@
intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
if (qcTile.getPrimaryAction() != null) {
try {
- qcTile.getPrimaryAction().send(getContext(), 0, intent);
+ qcTile.getPrimaryAction().send(getContext(), 0, intent,
+ /* requestCode= */ null,
+ /* fillInIntent= */ null,
+ /* options= */ null,
+ ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ .toBundle());
if (mActionListener != null) {
mActionListener.onQCAction(qcTile, qcTile.getPrimaryAction());
}
+ StatsLogHelper.getInstance().logMetrics(qcTile, intent);
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "Error sending intent", e);
}
@@ -145,6 +166,7 @@
if (mActionListener != null) {
mActionListener.onQCAction(qcTile, qcTile.getActionHandler());
}
+ StatsLogHelper.getInstance().logMetrics(qcTile, intent);
}
});
}
diff --git a/car-scalable-ui-lib/Android.bp b/car-scalable-ui-lib/Android.bp
new file mode 100644
index 0000000..23f8ec7
--- /dev/null
+++ b/car-scalable-ui-lib/Android.bp
@@ -0,0 +1,29 @@
+//
+// Copyright (C) 2024 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+ name: "car-scalable-ui-lib",
+ srcs: ["src/**/*.java"],
+ optimize: {
+ enabled: true,
+ },
+ static_libs: [
+ "androidx.annotation_annotation",
+ ],
+}
diff --git a/car-scalable-ui-lib/AndroidManifest.xml b/car-scalable-ui-lib/AndroidManifest.xml
new file mode 100644
index 0000000..d7d9bdc
--- /dev/null
+++ b/car-scalable-ui-lib/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.scalableui">
+</manifest>
diff --git a/car-scalable-ui-lib/OWNERS b/car-scalable-ui-lib/OWNERS
new file mode 100644
index 0000000..1ae9fdc
--- /dev/null
+++ b/car-scalable-ui-lib/OWNERS
@@ -0,0 +1,9 @@
+# People who can approve changes for submission.
+
+# Primary
+babakbo@google.com
+
+# Secondary (only if people in Primary are unreachable)
+alexstetson@google.com
+priyanksingh@google.com
+calhuang@google.com
diff --git a/car-scalable-ui-lib/PREUPLOAD.cfg b/car-scalable-ui-lib/PREUPLOAD.cfg
new file mode 100644
index 0000000..38f9800
--- /dev/null
+++ b/car-scalable-ui-lib/PREUPLOAD.cfg
@@ -0,0 +1,7 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+
+[Builtin Hooks]
+commit_msg_changeid_field = true
+commit_msg_test_field = true
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/manager/Event.java b/car-scalable-ui-lib/src/com/android/car/scalableui/manager/Event.java
new file mode 100644
index 0000000..6c40986
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/manager/Event.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.manager;
+
+/**
+ * Describes an event in the system. An event can optionally carry a payload object.
+ */
+public class Event {
+ private final String mId;
+ private final Object mPayload;
+
+ /**
+ * Constructs an Event without a payload.
+ *
+ * @param id A unique identifier associated with this event.
+ */
+ public Event(String id) {
+ this(id, null);
+ }
+
+ /**
+ * Constructs an Event with an optional payload.
+ *
+ * @param id A unique identifier associated with this event.
+ * @param payload An optional payload associated with this event.
+ */
+ public Event(String id, Object payload) {
+ mId = id;
+ mPayload = payload;
+ }
+
+ /**
+ * Returns the event identifier.
+ *
+ * @return The event identifier.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the payload associated with this event.
+ *
+ * @return The payload of the event, or null if no payload is associated.
+ */
+ public Object getPayload() {
+ return mPayload;
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/manager/EventDispatcher.java b/car-scalable-ui-lib/src/com/android/car/scalableui/manager/EventDispatcher.java
new file mode 100644
index 0000000..9543939
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/manager/EventDispatcher.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.manager;
+
+/**
+ * A utility class for dispatching events. This class provides methods for dispatching events with
+ * or without payloads. All events are handled by the {@link StateManager}.
+ */
+public class EventDispatcher {
+
+ /**
+ * Dispatches an event without a payload.
+ *
+ * @param eventId The id of the event that needs to be dispatched.
+ */
+ public static void dispatch(String eventId) {
+ dispatch(eventId, null);
+ }
+
+ /**
+ * Dispatches an event with a given payload.
+ *
+ * @param eventId The id of the event that needs to be dispatched.
+ * @param payload The payload associated with the event. Can be any Java object.
+ */
+ public static void dispatch(String eventId, Object payload) {
+ dispatch(new Event(eventId, payload));
+ }
+
+ /**
+ * Dispatches a given event.
+ *
+ * @param event The event object to be dispatched.
+ */
+ public static void dispatch(Event event) {
+ StateManager.handleEvent(event);
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/manager/StateManager.java b/car-scalable-ui-lib/src/com/android/car/scalableui/manager/StateManager.java
new file mode 100644
index 0000000..e3f6626
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/manager/StateManager.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.manager;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+
+import com.android.car.scalableui.model.PanelState;
+import com.android.car.scalableui.model.Transition;
+import com.android.car.scalableui.model.Variant;
+import com.android.car.scalableui.panel.Panel;
+import com.android.car.scalableui.panel.PanelPool;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the state of UI panels. This class is responsible for loading panel definitions,
+ * handling events that trigger state transitions, and applying visual updates to panels
+ * based on their current state.
+ */
+public class StateManager {
+
+ private static final StateManager sInstance = new StateManager();
+
+ private StateManager() {}
+
+ private final List<PanelState> mPanels = new ArrayList<>();
+
+ /**
+ * Adds a new panel state definition.
+ *
+ * @param panel The panel state to be added.
+ */
+ public static void addState(PanelState panel) {
+ sInstance.mPanels.add(panel);
+ applyState(panel);
+ }
+
+ /**
+ * Resets the state manager by clearing all panel definitions.
+ */
+ public static void reset() {
+ sInstance.mPanels.clear();
+ }
+
+ /**
+ * Handles an event by triggering state transitions for panels with matching transitions.
+ * This method iterates through all registered panel definitions, checks if any transitions
+ * are defined for the given event, and applies the transition (including animations) if found.
+ *
+ * @param event The event to be handled.
+ */
+ static void handleEvent(Event event) {
+ for (PanelState panelState : sInstance.mPanels) {
+ Transition transition = panelState.getTransition(event);
+ if (transition == null) {
+ continue;
+ }
+
+ Panel panel = PanelPool.getInstance().getPanel(panelState.getId());
+ Animator animator = transition.getAnimator(panel, panelState.getCurrentVariant());
+ if (animator != null) {
+ // Update the internal state to the new variant and show the transition animation
+ panelState.onAnimationStart(animator);
+ panelState.setVariant(transition.getToVariant().getId(), event.getPayload());
+ animator.removeAllListeners();
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ panelState.onAnimationEnd();
+ applyState(panelState);
+ }
+ });
+ animator.start();
+ } else if (!panelState.isAnimating()) {
+ // Force apply the new state if there is no on going animation.
+ Variant toVariant = transition.getToVariant();
+ panelState.setVariant(toVariant.getId(), event.getPayload());
+ applyState(panelState);
+ }
+ }
+ }
+
+ /**
+ * Applies the current state of a panel to the UI. This method updates the panel's
+ * visual properties (bounds, visibility, alpha, layer) based on its current variant.
+ *
+ * @param panelState The panel data containing the current state information.
+ */
+ private static void applyState(PanelState panelState) {
+ Variant variant = panelState.getCurrentVariant();
+ String panelId = panelState.getId();
+ Panel panel = PanelPool.getInstance().getPanel(panelId);
+ panel.setRole(panelState.getRole().getValue());
+ panel.setBounds(variant.getBounds());
+ panel.setVisibility(variant.isVisible());
+ panel.setAlpha(variant.getAlpha());
+ panel.setLayer(variant.getLayer());
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/Alpha.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Alpha.java
new file mode 100644
index 0000000..ab70d3d
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Alpha.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Represents the alpha (transparency) value of a UI element. This class provides methods for
+ * creating an Alpha object from an XML definition and retrieving the alpha value.
+ */
+class Alpha {
+ static final String ALPHA_TAG = "Alpha";
+ private static final String ALPHA_ATTRIBUTE = "alpha";
+ static final float DEFAULT_ALPHA = 1;
+
+ private final float mAlpha;
+
+ /**
+ * Constructs an Alpha object with the specified alpha value.
+ *
+ * @param alpha The alpha value, between 0 (fully transparent) and 1 (fully opaque).
+ */
+ Alpha(float alpha) {
+ mAlpha = alpha;
+ }
+
+ /**
+ * Returns the alpha value.
+ *
+ * @return The alpha value.
+ */
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ /**
+ * Creates an Alpha object from an XML parser.
+ *
+ * This method parses an XML element with the tag "Alpha" and extracts the "alpha" attribute
+ * to create an Alpha object. If the "alpha" attribute is not specified, it defaults to 1.0.
+ *
+ * @param parser The XML parser.
+ * @return An Alpha object with the parsed alpha value.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ static Alpha create(XmlPullParser parser) throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, ALPHA_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ float alpha = attrs.getAttributeFloatValue(null, ALPHA_ATTRIBUTE, DEFAULT_ALPHA);
+ parser.nextTag();
+ parser.require(XmlPullParser.END_TAG, null, ALPHA_TAG);
+ return new Alpha(alpha);
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/Bounds.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Bounds.java
new file mode 100644
index 0000000..13041b0
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Bounds.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+
+/**
+ * Represents the bounds of a UI element. This class provides methods for creating a Bounds object
+ * from an XML definition and retrieving the bounds as a {@link Rect}.
+ *
+ * <p>The Bounds class supports defining dimensions in the following formats:
+ * <ul>
+ * <li><b>Absolute pixels:</b> e.g., <code>left="100"</code></li>
+ * <li><b>Density-independent pixels (dp):</b> e.g., <code>top="50dip"</code></li>
+ * <li><b>Percentage of screen width/height:</b> e.g., <code>right="80%"</code></li>
+ * <li><b>Resource references:</b> e.g., <code>bottom="@dimen/my_bottom_margin"</code></li>
+ * </ul>
+ *
+ * <p>It also allows defining either the left and right positions, or the left position and width.
+ * Similarly, it allows defining either the top and bottom positions, or the top position and
+ * height.
+ */
+class Bounds {
+ static final String BOUNDS_TAG = "Bounds";
+ private static final String LEFT_ATTRIBUTE = "left";
+ private static final String RIGHT_ATTRIBUTE = "right";
+ private static final String TOP_ATTRIBUTE = "top";
+ private static final String BOTTOM_ATTRIBUTE = "bottom";
+ private static final String WIDTH_ATTRIBUTE = "width";
+ private static final String HEIGHT_ATTRIBUTE = "height";
+ private static final String DIP = "dip";
+ private static final String DP = "dp";
+ private static final String PERCENT = "%";
+ private final int mLeft;
+ private final int mTop;
+ private final int mRight;
+ private final int mBottom;
+
+ /**
+ * Constructs a Bounds object with the specified left, top, right, and bottom positions.
+ *
+ * @param left The left position in pixels.
+ * @param top The top position in pixels.
+ * @param right The right position in pixels.
+ * @param bottom The bottom position in pixels.
+ */
+ Bounds(int left, int top, int right, int bottom) {
+ mLeft = left;
+ mTop = top;
+ mRight = right;
+ mBottom = bottom;
+ }
+
+ /**
+ * Returns the bounds as a {@link Rect} object.
+ *
+ * @return A Rect object representing the bounds.
+ */
+ public Rect getRect() {
+ return new Rect(mLeft, mTop, mRight, mBottom);
+ }
+
+ /**
+ * Creates a Bounds object from an XML parser.
+ *
+ * <p>This method parses an XML element with the tag "Bounds" and extracts the "left", "top",
+ * "right", and "bottom" attributes (or equivalent width/height combinations) to create a
+ * Bounds object.
+ *
+ * @param context The application context.
+ * @param parser The XML parser.
+ * @return A Bounds object with the parsed bounds.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ static Bounds create(Context context, XmlPullParser parser) throws XmlPullParserException,
+ IOException {
+ parser.require(XmlPullParser.START_TAG, null, BOUNDS_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ int left = getDimensionPixelSize(context, attrs, LEFT_ATTRIBUTE, true);
+ int top = getDimensionPixelSize(context, attrs, TOP_ATTRIBUTE, false);
+ int right = getDimensionPixelSize(context, attrs, RIGHT_ATTRIBUTE, true);
+ int bottom = getDimensionPixelSize(context, attrs, BOTTOM_ATTRIBUTE, false);
+
+ int width = getDimensionPixelSize(context, attrs, WIDTH_ATTRIBUTE, true);
+ int height = getDimensionPixelSize(context, attrs, HEIGHT_ATTRIBUTE, false);
+ if (attrs.getAttributeValue(null, RIGHT_ATTRIBUTE) == null) {
+ right = left + width;
+ } else if (attrs.getAttributeValue(null, LEFT_ATTRIBUTE) == null) {
+ left = right - width;
+ }
+ if (attrs.getAttributeValue(null, BOTTOM_ATTRIBUTE) == null) {
+ bottom = top + height;
+ } else if (attrs.getAttributeValue(null, TOP_ATTRIBUTE) == null) {
+ top = bottom - height;
+ }
+
+ parser.nextTag();
+ parser.require(XmlPullParser.END_TAG, null, BOUNDS_TAG);
+ return new Bounds(left, top, right, bottom);
+ }
+
+ /**
+ * Helper method to get a dimension pixel size from an attribute set.
+ *
+ * @param context The application context.
+ * @param attrs The attribute set.
+ * @param name The name of the attribute.
+ * @param isHorizontal Whether the dimension is horizontal (width) or vertical (height).
+ * @return The dimension pixel size.
+ */
+ private static int getDimensionPixelSize(Context context, AttributeSet attrs, String name,
+ boolean isHorizontal) {
+ int resId = attrs.getAttributeResourceValue(null, name, 0);
+ if (resId != 0) {
+ return context.getResources().getDimensionPixelSize(resId);
+ }
+ String dimenStr = attrs.getAttributeValue(null, name);
+ if (dimenStr == null) {
+ return 0;
+ }
+ if (dimenStr.toLowerCase().endsWith(DP)) {
+ String valueStr = dimenStr.substring(0, dimenStr.length() - DP.length());
+ float value = Float.parseFloat(valueStr);
+ return (int) (value * Resources.getSystem().getDisplayMetrics().density);
+ } else if (dimenStr.toLowerCase().endsWith(DIP)) {
+ String valueStr = dimenStr.substring(0, dimenStr.length() - DIP.length());
+ float value = Float.parseFloat(valueStr);
+ return (int) (value * Resources.getSystem().getDisplayMetrics().density);
+ } else if (dimenStr.toLowerCase().endsWith(PERCENT)) {
+ String valueStr = dimenStr.substring(0, dimenStr.length() - PERCENT.length());
+ float value = Float.parseFloat(valueStr);
+ DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
+ if (isHorizontal) {
+ return (int) (value * displayMetrics.widthPixels / 100);
+ } else {
+ return (int) (value * displayMetrics.heightPixels / 100);
+ }
+ } else {
+ return attrs.getAttributeIntValue(null, name, 0);
+ }
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/KeyFrameVariant.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/KeyFrameVariant.java
new file mode 100644
index 0000000..de961fc
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/KeyFrameVariant.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import android.animation.FloatEvaluator;
+import android.animation.RectEvaluator;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A {@link Variant} that interpolates between different variants based on a fraction value.
+ *
+ * <p>This class defines a series of keyframes, each associated with a {@link Variant} and a frame
+ * position. The {@link #setFraction(float)} method sets the current fraction, which determines
+ * the interpolation between keyframes.</p>
+ *
+ * <p>KeyFrameVariant allows for smooth transitions between different panel states by interpolating
+ * properties such as bounds, visibility, and alpha.
+ */
+public class KeyFrameVariant extends Variant {
+ static final String KEY_FRAME_VARIANT_TAG = "KeyFrameVariant";
+ private static final String ID_ATTRIBUTE = "id";
+ private static final String PARENT_ATTRIBUTE = "parent";
+ private static final String KEY_FRAME_TAG = "KeyFrame";
+ private static final String FRAME_ATTRIBUTE = "frame";
+ private static final String VARIANT_ATTRIBUTE = "variant";
+
+ private float mFraction;
+ private final RectEvaluator mRectEvaluator = new RectEvaluator();
+ private final FloatEvaluator mFloatEvaluator = new FloatEvaluator();
+
+ /**
+ * Represents a single keyframe in a {@link KeyFrameVariant}.
+ */
+ public static class KeyFrame {
+ int mFramePosition;
+ Variant mVariant;
+
+ /**
+ * Constructor for KeyFrame.
+ *
+ * @param framePosition The position of the keyframe (0-100).
+ * @param variant The variant associated with this keyframe.
+ */
+ public KeyFrame(int framePosition, Variant variant) {
+ mFramePosition = framePosition;
+ mVariant = variant;
+ }
+
+ /**
+ * Reads a {@link KeyFrame} from an XMLPullParser.
+ *
+ * @param panelState The current panel state.
+ * @param parser The XML parser.
+ * @return The created KeyFrame.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ private static KeyFrame create(PanelState panelState, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, KEY_FRAME_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ int frame = attrs.getAttributeIntValue(null, FRAME_ATTRIBUTE, 0);
+ String variant = attrs.getAttributeValue(null, VARIANT_ATTRIBUTE);
+ parser.nextTag();
+ parser.require(XmlPullParser.END_TAG, null, KEY_FRAME_TAG);
+ Variant panelVariant = panelState.getVariant(variant);
+ return new KeyFrameVariant.KeyFrame(frame, panelVariant);
+ }
+ }
+
+ private final List<KeyFrame> mKeyFrames = new ArrayList<>();
+
+ /**
+ * Constructor for KeyFrameVariant.
+ *
+ * @param id The ID of this variant.
+ * @param base The base variant to inherit properties from.
+ */
+ public KeyFrameVariant(String id, Variant base) {
+ super(id, base);
+ }
+
+ /**
+ * Adds a keyframe to this variant.
+ *
+ * @param keyFrame The keyframe to add.
+ */
+ public void addKeyFrame(KeyFrame keyFrame) {
+ mKeyFrames.add(keyFrame);
+ mKeyFrames.sort(Comparator.comparingInt(o -> o.mFramePosition));
+ }
+
+ /**
+ * Sets the current fraction for interpolation.
+ *
+ * @param fraction The fraction value (between 0 and 1).
+ */
+ public void setFraction(float fraction) {
+ mFraction = fraction;
+ }
+
+ /**
+ * Returns the interpolated bounds for the current fraction.
+ *
+ * @return The interpolated bounds.
+ */
+ public Rect getBounds() {
+ return getBounds(mFraction);
+ }
+
+ /**
+ * Returns the interpolated visibility for the current fraction.
+ *
+ * @return The interpolated visibility.
+ */
+ public boolean isVisible() {
+ return getVisibility(mFraction);
+ }
+
+ /**
+ * Returns the interpolated alpha for the current fraction.
+ *
+ * @return The interpolated alpha.
+ */
+ public float getAlpha() {
+ return getAlpha(mFraction);
+ }
+
+ /**
+ * Sets the payload for this variant.
+ *
+ * <p>The payload is expected to be a float value representing the fraction.
+ *
+ * @param payload The payload object.
+ */
+ public void setPayload(Object payload) {
+ setFraction((float) payload);
+ }
+
+ /**
+ * Finds the keyframe immediately before the given fraction.
+ *
+ * <p>This method iterates through the list of keyframes and returns the keyframe that is
+ * immediately before the given fraction. If the fraction is smaller than the first keyframe's
+ * position, the first keyframe is returned. If the fraction is larger than the last keyframe's
+ * position, the last keyframe is returned.
+ *
+ * @param fraction The fraction value (between 0 and 1).
+ * @return The keyframe before the given fraction, or null if there are no keyframes.
+ */
+ private KeyFrame before(float fraction) {
+ if (mKeyFrames.isEmpty()) return null;
+ KeyFrame current = mKeyFrames.get(0);
+ for (KeyFrame keyFrame : mKeyFrames) {
+ if (keyFrame.mFramePosition >= fraction * 100) {
+ return current;
+ }
+ current = keyFrame;
+ }
+ return current;
+ }
+
+ /**
+ * Returns the key frame after the fraction
+ * @param fraction The fraction value (between 0 and 1).
+ * @return The key frame
+ */
+ private KeyFrame after(float fraction) {
+ if (mKeyFrames.isEmpty()) return null;
+ for (KeyFrame keyFrame : mKeyFrames) {
+ if (keyFrame.mFramePosition >= fraction * 100) {
+ return keyFrame;
+ }
+ }
+ return mKeyFrames.get(mKeyFrames.size() - 1);
+ }
+
+ /**
+ * Calculates the fraction between two keyframes based on the given overall fraction.
+ *
+ * <p>This method takes two frame positions (representing keyframes) and an overall fraction
+ * value (between 0 and 1). It calculates the fraction between the two keyframes, effectively
+ * normalizing the overall fraction to the range between the keyframes.
+ *
+ * <p>For example, if framePosition1 is 20, framePosition2 is 80, and fraction is 0.5, the
+ * result will be 0.75, because 0.5 lies at 75% of the range between 20 and 80.
+ *
+ * @param framePosition1 The position of the first keyframe (0-100).
+ * @param framePosition2 The position of the second keyframe (0-100).
+ * @param fraction The overall fraction value (between 0 and 1).
+ * @return The fraction between the two keyframes.
+ */
+ private float getKeyFrameFraction(int framePosition1, int framePosition2, float fraction) {
+ fraction = fraction * 100;
+ if (fraction <= framePosition1) return 0;
+ if (fraction >= framePosition2) return 1;
+ return (fraction - framePosition1) / (framePosition2 - framePosition1);
+ }
+
+
+ /**
+ * Returns the interpolated bounds for the given fraction.
+ *
+ * @param fraction The fraction value (between 0 and 1).
+ * @return The interpolated bounds.
+ */
+ private Rect getBounds(float fraction) {
+ if (mKeyFrames.isEmpty()) return new Rect();
+ KeyFrame keyFrame1 = before(fraction);
+ Rect bounds1 = Objects.requireNonNull(keyFrame1).mVariant.getBounds();
+ KeyFrame keyFrame2 = after(fraction);
+ Rect bounds2 = Objects.requireNonNull(keyFrame2).mVariant.getBounds();
+ float fractionInBetween = getKeyFrameFraction(keyFrame1.mFramePosition,
+ keyFrame2.mFramePosition, fraction);
+ Rect rect = mRectEvaluator.evaluate(fractionInBetween, bounds1, bounds2);
+ return new Rect(rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ /**
+ * Returns the interpolated visibility for the given fraction.
+ *
+ * @param fraction The fraction value (between 0 and 1).
+ * @return The interpolated visibility.
+ */
+ private boolean getVisibility(float fraction) {
+ if (mKeyFrames.isEmpty()) return false;
+ KeyFrame keyFrame1 = before(fraction);
+ boolean isVisible1 = Objects.requireNonNull(keyFrame1).mVariant.isVisible();
+ KeyFrame keyFrame2 = after(fraction);
+ boolean isVisible2 = Objects.requireNonNull(keyFrame2).mVariant.isVisible();
+ return isVisible1 || isVisible2;
+ }
+
+ /**
+ * Returns the interpolated alpha for the given fraction.
+ *
+ * @param fraction The fraction value (between 0 and 1).
+ * @return The interpolated alpha.
+ */
+ private float getAlpha(float fraction) {
+ if (mKeyFrames.isEmpty()) return 1;
+ KeyFrame keyFrame1 = before(fraction);
+ float alpha1 = (Objects.requireNonNull(keyFrame1).mVariant.getAlpha());
+ KeyFrame keyFrame2 = after(fraction);
+ float alpha2 = (Objects.requireNonNull(keyFrame2).mVariant.getAlpha());
+ return mFloatEvaluator.evaluate(fraction, alpha1, alpha2);
+ }
+
+ /**
+ * Creates a {@link KeyFrameVariant} from an XMLPullParser.
+ *
+ * @param panelState The current panel state.
+ * @param parser The XML parser.
+ * @return The created KeyFrameVariant.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ static KeyFrameVariant create(PanelState panelState, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, KEY_FRAME_VARIANT_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ String id = attrs.getAttributeValue(null, ID_ATTRIBUTE);
+ String parentStr = attrs.getAttributeValue(null, PARENT_ATTRIBUTE);
+ Variant parent = panelState.getVariant(parentStr);
+ KeyFrameVariant result = new KeyFrameVariant(id, parent);
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) continue;
+ String name = parser.getName();
+ if (name.equals(KEY_FRAME_TAG)) {
+ result.addKeyFrame(KeyFrame.create(panelState, parser));
+ } else {
+ XmlPullParserHelper.skip(parser);
+ }
+ }
+ return result;
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/Layer.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Layer.java
new file mode 100644
index 0000000..e587728
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Layer.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Represents the layer of a {@code Panel}. This class provides methods for creating a Layer object
+ * from an XML definition and retrieving the layer value.
+ */
+class Layer {
+ static final String LAYER_TAG = "Layer";
+ private static final String LAYER_ATTRIBUTE = "layer";
+
+ static final int DEFAULT_LAYER = 0;
+
+ private final int mLayer;
+
+ /**
+ * Constructs a Layer object with the specified layer value.
+ *
+ * @param layer The layer value. Higher values indicate that the element should be drawn on top
+ * of elements with lower layer values.
+ */
+ Layer(int layer) {
+ mLayer = layer;
+ }
+
+ /**
+ * Returns the layer value.
+ *
+ * @return The layer value.
+ */
+ public int getLayer() {
+ return mLayer;
+ }
+
+ /**
+ * Creates a Layer object from an XML parser.
+ *
+ * <p>This method parses an XML element with the tag "Layer" and extracts the "layer" attribute
+ * to create a Layer object. If the "layer" attribute is not specified, it defaults to 0.
+ *
+ * @param parser The XML parser.
+ * @return A Layer object with the parsed layer value.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ static Layer create(XmlPullParser parser) throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, LAYER_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ int layer = attrs.getAttributeIntValue(null, LAYER_ATTRIBUTE, DEFAULT_LAYER);
+ parser.nextTag();
+ parser.require(XmlPullParser.END_TAG, null, LAYER_TAG);
+ return new Layer(layer);
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/PanelState.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/PanelState.java
new file mode 100644
index 0000000..d8ac257
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/PanelState.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import static com.android.car.scalableui.model.KeyFrameVariant.KEY_FRAME_VARIANT_TAG;
+import static com.android.car.scalableui.model.Transition.TRANSITION_TAG;
+import static com.android.car.scalableui.model.Variant.VARIANT_TAG;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import com.android.car.scalableui.manager.Event;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents the state of a panel in the Scalable UI system.
+ *
+ * <p>A PanelState defines the different variants (layouts) that a panel can have, as well as the
+ * transitions between those variants. It also manages the current variant and any running
+ * animations.
+ */
+public class PanelState {
+ private static final String PANEL_TAG = "Panel";
+ private static final String ID_TAG = "id";
+ private static final String DEFAULT_VARIANT_ATTRIBUTE = "defaultVariant";
+ private static final String ROLE_ATTRIBUTE = "role";
+ private static final String TRANSITIONS_TAG = "Transitions";
+ private static final String DEFAULT_DURATION_ATTRIBUTE = "defaultDuration";
+ private static final String DEFAULT_INTERPOLATOR_ATTRIBUTE = "defaultInterpolator";
+ private static final int DEFAULT_TRANSITION_DURATION = 300;
+
+ /**
+ * Loads a PanelState from an XML resource.
+ *
+ * @param context The context to use.
+ * @param resourceId The ID of the XML resource.
+ * @return The loaded PanelState.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ public static PanelState load(Context context, int resourceId) throws XmlPullParserException,
+ IOException {
+ XmlResourceParser parser = context.getResources().getXml(resourceId);
+ while (true) {
+ if (parser.next() == XmlPullParser.START_TAG) break;
+ }
+ return PanelState.create(context, parser);
+ }
+
+ private final String mId;
+ private final Role mRole;
+ private final List<Variant> mVariants = new ArrayList<>();
+ private final List<Transition> mTransitions = new ArrayList<>();
+
+ private Animator mRunningAnimator;
+ private Variant mCurrentVariant;
+
+ /**
+ * Constructor for PanelState.
+ *
+ * @param id The ID of the panel.
+ * @param role The role of the panel.
+ */
+ public PanelState(String id, Role role) {
+ mId = id;
+ mRole = role;
+ }
+
+ /**
+ * Returns the ID of the panel.
+ *
+ * @return The ID of the panel.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Adds a variant to the panel.
+ *
+ * @param variant The variant to add.
+ */
+ public void addVariant(Variant variant) {
+ mVariants.add(variant);
+ }
+
+ /**
+ * Adds a transition to the panel.
+ *
+ * @param transition The transition to add.
+ */
+ public void addTransition(Transition transition) {
+ mTransitions.add(transition);
+ }
+
+ /**
+ * Returns the current variant of the panel.
+ *
+ * @return The current variant of the panel.
+ */
+ public Variant getCurrentVariant() {
+ if (mCurrentVariant == null) {
+ mCurrentVariant = mVariants.get(0);
+ }
+ return mCurrentVariant;
+ }
+
+ /**
+ * Returns the variant with the given ID.
+ *
+ * @param id The ID of the variant.
+ * @return The variant with the given ID, or null if not found.
+ */
+ public Variant getVariant(String id) {
+ for (Variant variant : mVariants) {
+ if (variant.getId().equals(id)) {
+ return variant;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the current variant to the variant with the given ID.
+ *
+ * @param id The ID of the variant.
+ */
+ public void setVariant(String id) {
+ setVariant(id, null);
+ }
+
+ /**
+ * Sets the current variant to the variant with the given ID and payload.
+ *
+ * @param id The ID of the variant.
+ * @param payload The payload to pass to the variant.
+ */
+ public void setVariant(String id, Object payload) {
+ for (Variant variant : mVariants) {
+ if (variant.getId().equals(id)) {
+ mCurrentVariant = variant;
+ if (mCurrentVariant instanceof KeyFrameVariant) {
+ ((KeyFrameVariant) mCurrentVariant).setPayload(payload);
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Returns the role of the panel.
+ *
+ * @return The role of the panel.
+ */
+ public Role getRole() {
+ return mRole;
+ }
+
+ /**
+ * Returns true if the panel is currently animating.
+ *
+ * @return True if the panel is currently animating.
+ */
+ public boolean isAnimating() {
+ return mRunningAnimator != null && mRunningAnimator.isRunning();
+ }
+
+ /**
+ * Should be called when an animation starts.
+ *
+ * @param animator The animator that started.
+ */
+ public void onAnimationStart(Animator animator) {
+ if (mRunningAnimator != null) {
+ mRunningAnimator.pause();
+ mRunningAnimator.removeAllListeners();
+ }
+ mRunningAnimator = animator;
+ }
+
+ /**
+ * Should be Called when an animation ends.
+ */
+ public void onAnimationEnd() {
+ mRunningAnimator = null;
+ }
+
+ /**
+ * Returns the transition for the given event.
+ *
+ * @param event The event.
+ * @return The transition for the given event, or null if not found.
+ */
+ public Transition getTransition(Event event) {
+ // If both onEvent and fromVariant matches
+ Transition result = getTransition(event.getId(), getCurrentVariant().getId());
+ if (result != null) {
+ return result;
+ }
+ // If only onEvent matches
+ return getTransition(event.getId());
+ }
+
+ /**
+ * Returns a transition that matches the given event ID and "from" variant.
+ *
+ * @param eventId The ID of the event to find a transition for.
+ * @param fromVariant The ID of the variant the transition should start from.
+ * @return The matching transition, or null if no such transition is found.
+ */
+ private Transition getTransition(String eventId, String fromVariant) {
+ for (Transition transition : mTransitions) {
+ if (eventId.equals(transition.getOnEvent())
+ && transition.getFromVariant() != null
+ && transition.getFromVariant().getId().equals(fromVariant)) {
+ return transition;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a transition that matches the given event ID and has no "from" variant specified.
+ *
+ * @param eventId The ID of the event to find a transition for.
+ * @return The matching transition, or null if no such transition is found.
+ */
+ private Transition getTransition(String eventId) {
+ for (Transition transition : mTransitions) {
+ if (eventId.equals(transition.getOnEvent())
+ && transition.getFromVariant() == null) {
+ return transition;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Creates a PanelState object from an XML parser.
+ *
+ * <p>This method parses an XML element with the tag "Panel" and extracts its attributes
+ * and child elements to create a Panel object.
+ *
+ * @param context The application context.
+ * @param parser The XML parser.
+ * @return A PanelState object with the parsed properties.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ private static PanelState create(Context context, XmlPullParser parser) throws
+ XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, PANEL_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ String id = attrs.getAttributeValue(null, ID_TAG);
+ String defaultVariant = attrs.getAttributeValue(null, DEFAULT_VARIANT_ATTRIBUTE);
+ int roleValue = attrs.getAttributeResourceValue(null, ROLE_ATTRIBUTE, 0);
+ PanelState result = new PanelState(id, new Role(roleValue));
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) continue;
+ String name = parser.getName();
+ switch (name) {
+ case VARIANT_TAG:
+ Variant variant = Variant.create(context, result, parser);
+ result.addVariant(variant);
+ break;
+ case KEY_FRAME_VARIANT_TAG:
+ KeyFrameVariant keyFrameVariant = KeyFrameVariant.create(result, parser);
+ result.addVariant(keyFrameVariant);
+ break;
+ case TRANSITIONS_TAG:
+ List<Transition> transitions = readTransitions(context, result, parser);
+ for (Transition transition : transitions) {
+ result.addTransition(transition);
+ }
+ break;
+ default:
+ XmlPullParserHelper.skip(parser);
+ break;
+ }
+ }
+ result.setVariant(defaultVariant);
+ return result;
+ }
+
+ /**
+ * Reads a list of Transition objects from an XML parser.
+ *
+ * <p>This method parses an XML element with the tag "Transitions" and extracts its attributes
+ * and child transition elements.
+ *
+ * @param context The application context.
+ * @param parser The XML parser.
+ * @return A list of Transition objects with the parsed properties.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ private static List<Transition> readTransitions(Context context, PanelState panelState,
+ XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, TRANSITIONS_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ int duration = attrs.getAttributeIntValue(null,
+ DEFAULT_DURATION_ATTRIBUTE, DEFAULT_TRANSITION_DURATION);
+ int interpolatorRef = attrs.getAttributeResourceValue(null,
+ DEFAULT_INTERPOLATOR_ATTRIBUTE, 0);
+ Interpolator interpolator = interpolatorRef == 0 ? null :
+ AnimationUtils.loadInterpolator(context, interpolatorRef);
+
+ List<Transition> result = new ArrayList<>();
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) continue;
+
+ if (parser.getName().equals(TRANSITION_TAG)) {
+ result.add(Transition.create(context, panelState, duration, interpolator, parser));
+ } else {
+ XmlPullParserHelper.skip(parser);
+ }
+ }
+ return result;
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/Role.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Role.java
new file mode 100644
index 0000000..c2d4ef7
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Role.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+/**
+ * Represents the role of a {@code Panel} within the system.
+ *
+ * <p>This class encapsulates an integer value that signifies the role of a UI element.
+ * The specific meaning of the role value is determined by the system using it.
+ */
+public class Role {
+ private final int mValue;
+
+ /**
+ * Constructor for Role.
+ *
+ * @param value The integer value representing the role.
+ */
+ public Role(int value) {
+ mValue = value;
+ }
+
+ /**
+ * Returns the integer value representing the role.
+ *
+ * @return The integer value of the role.
+ */
+ public int getValue() {
+ return mValue;
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/Transition.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Transition.java
new file mode 100644
index 0000000..d8dd997
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Transition.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.NonNull;
+
+import com.android.car.scalableui.panel.Panel;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Represents a transition between two {@link Variant}s in the Scalable UI system.
+ *
+ * <p>A Transition defines the animation that should be used to transition from one variant to
+ * another in response to an event. It can optionally specify a specific "from" variant, a "to"
+ * variant, an event trigger, and a custom animator.
+ */
+public class Transition {
+ public static final String TRANSITION_TAG = "Transition";
+ private static final String FROM_VARIANT_ATTRIBUTE = "fromVariant";
+ private static final String TO_VARIANT_ATTRIBUTE = "toVariant";
+ private static final String ON_EVENT_ATTRIBUTE = "onEvent";
+ private static final String ANIMATOR_ATTRIBUTE = "animator";
+ private static final long DEFAULT_DURATION = 300;
+
+ private final Variant mFromVariant;
+ @NonNull
+ private final Variant mToVariant;
+ private final String mOnEvent;
+ private final Animator mAnimator;
+ private final Interpolator mDefaultInterpolator;
+ private final long mDefaultDuration;
+
+ /**
+ * Constructor for Transition.
+ *
+ * @param fromVariant The variant to transition from (can be null).
+ * @param toVariant The variant to transition to.
+ * @param onEvent The event that triggers the transition.
+ * @param animator A custom animator to use for the transition (can be null).
+ * @param defaultDuration The default duration of the transition.
+ * @param defaultInterpolator The default interpolator to use for the transition.
+ */
+ public Transition(Variant fromVariant, @NonNull Variant toVariant, String onEvent,
+ Animator animator, long defaultDuration, Interpolator defaultInterpolator) {
+ mFromVariant = fromVariant;
+ mToVariant = toVariant;
+ mAnimator = animator;
+ mOnEvent = onEvent;
+ mDefaultDuration = defaultDuration >= 0 ? defaultDuration : DEFAULT_DURATION;
+ mDefaultInterpolator = defaultInterpolator != null
+ ? defaultInterpolator
+ : new AccelerateDecelerateInterpolator();
+ }
+
+ /**
+ * Returns the "from" variant of the transition.
+ *
+ * @return The "from" variant, or null if not specified.
+ */
+ public Variant getFromVariant() {
+ return mFromVariant;
+ }
+
+ /**
+ * Returns the "to" variant of the transition.
+ *
+ * @return The "to" variant.
+ */
+ public @NonNull Variant getToVariant() {
+ return mToVariant;
+ }
+
+ /**
+ * Returns the animator for the transition.
+ *
+ * <p>If a custom animator was provided, it is cloned and returned. Otherwise, a default
+ * animator will be created to transition from "from" variant to "to" variant with the default
+ * duration and interpolator.
+ *
+ * @param panel The panel to apply the animation to.
+ * @param fromVariant The actual "from" variant of the transition.
+ * @return The animator for the transition.
+ */
+ public Animator getAnimator(Panel panel, @NonNull Variant fromVariant) {
+ if (fromVariant.getId().equals(mToVariant.getId())) {
+ return null;
+ }
+
+ if (mAnimator != null) {
+ Animator animator = this.mAnimator.clone();
+ animator.setTarget(panel);
+ return animator;
+ }
+ return fromVariant.getAnimator(panel, mToVariant, mDefaultDuration, mDefaultInterpolator);
+ }
+
+ /**
+ * Returns the event that triggers the transition.
+ *
+ * @return The event that triggers the transition.
+ */
+ public String getOnEvent() {
+ return mOnEvent;
+ }
+
+ /**
+ * Creates a Transition object from an XML parser.
+ *
+ * @param context The context to use.
+ * @param panelState The panel state that this transition belongs to.
+ * @param defaultDuration The default duration to use if not specified in the XML.
+ * @param defaultInterpolator The default interpolator to use if not specified in the XML.
+ * @param parser The XML parser.
+ * @return The created Transition object.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ public static Transition create(Context context, PanelState panelState, long defaultDuration,
+ Interpolator defaultInterpolator, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, TRANSITION_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ String from = attrs.getAttributeValue(null, FROM_VARIANT_ATTRIBUTE);
+ String to = attrs.getAttributeValue(null, TO_VARIANT_ATTRIBUTE);
+ String onEvent = attrs.getAttributeValue(null, ON_EVENT_ATTRIBUTE);
+ int animatorId = attrs.getAttributeResourceValue(null, ANIMATOR_ATTRIBUTE, 0);
+ Animator animator = animatorId == 0
+ ? null
+ : AnimatorInflater.loadAnimator(context, animatorId);
+ Variant fromVariant = panelState.getVariant(from);
+ Variant toVariant = panelState.getVariant(to);
+ Transition result = new Transition(fromVariant, toVariant, onEvent, animator,
+ defaultDuration, defaultInterpolator);
+ parser.nextTag();
+ parser.require(XmlPullParser.END_TAG, null, TRANSITION_TAG);
+ return result;
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/Variant.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Variant.java
new file mode 100644
index 0000000..ba2e1b5
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Variant.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import android.animation.Animator;
+import android.animation.FloatEvaluator;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.animation.Interpolator;
+
+import com.android.car.scalableui.panel.Panel;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Represents a specific visual state or variant of a {@code Panel}.
+ *
+ * <p>This class defines the visual properties of a {@code Panel}, such as its bounds,
+ * visibility, layer, and alpha. It also provides methods for creating animations
+ * to transition between different variants.
+ */
+public class Variant {
+ static final String VARIANT_TAG = "Variant";
+ private static final String ID_ATTRIBUTE = "id";
+ private static final String PARENT_ATTRIBUTE = "parent";
+
+ private final FloatEvaluator mFloatEvaluator = new FloatEvaluator();
+ private final RectEvaluator mRectEvaluator = new RectEvaluator();
+
+ private final String mId;
+ private float mAlpha;
+ private boolean mIsVisible;
+ private int mLayer;
+ private Rect mBounds;
+
+ /**
+ * Constructs a Variant object with the specified ID and optional base variant.
+ *
+ * <p>If a base variant is provided, the new variant inherits its visual properties.
+ *
+ * @param id The ID of the variant.
+ * @param base The optional base variant to inherit properties from.
+ */
+ public Variant(String id, Variant base) {
+ this.mId = id;
+ if (base != null) {
+ mBounds = base.getBounds();
+ mIsVisible = base.isVisible();
+ mLayer = base.getLayer();
+ mAlpha = base.getAlpha();
+ } else {
+ mBounds = new Rect();
+ mIsVisible = Visibility.DEFAULT_VISIBILITY;
+ mLayer = Layer.DEFAULT_LAYER;
+ mAlpha = Alpha.DEFAULT_ALPHA;
+ }
+ }
+
+ /**
+ * Returns the ID of the variant.
+ *
+ * @return The ID of the variant.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Creates an animator to transition from the current state of a panel to this variant.
+ *
+ * @param panel The panel to animate.
+ * @param toVariant The target variant to animate to.
+ * @param duration The duration of the animation.
+ * @param interpolator The interpolator to use for the animation.
+ * @return An animator that animates the panel's properties to the target variant.
+ */
+ public Animator getAnimator(Panel panel, Variant toVariant, long duration,
+ Interpolator interpolator) {
+ if (toVariant instanceof KeyFrameVariant) {
+ return null;
+ } else {
+ float fromAlpha = panel.getAlpha();
+ float toAlpha = toVariant.getAlpha();
+ Rect fromBounds = panel.getBounds();
+ Rect toBounds = toVariant.getBounds();
+ boolean isVisible = panel.isVisible() || toVariant.isVisible();
+ int layer = toVariant.getLayer();
+ ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
+ valueAnimator.setDuration(duration);
+ valueAnimator.setInterpolator(interpolator);
+ valueAnimator.addUpdateListener(animator -> {
+ panel.setVisibility(isVisible);
+ panel.setLayer(layer);
+ float fraction = animator.getAnimatedFraction();
+ Rect bounds = mRectEvaluator.evaluate(fraction, fromBounds, toBounds);
+ panel.setBounds(bounds);
+ float alpha = mFloatEvaluator.evaluate(fraction, fromAlpha, toAlpha);
+ panel.setAlpha(alpha);
+ });
+ return valueAnimator;
+ }
+ }
+
+ /**
+ * Returns whether the variant is visible.
+ *
+ * @return True if the variant is visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return mIsVisible;
+ }
+
+ /**
+ * Sets the visibility of the variant.
+ *
+ * @param isVisible True if the variant should be visible, false otherwise.
+ */
+ public void setVisibility(boolean isVisible) {
+ this.mIsVisible = isVisible;
+ }
+
+ /**
+ * Returns the layer of the variant.
+ *
+ * @return The layer of the variant.
+ */
+ public int getLayer() {
+ return mLayer;
+ }
+
+ /**
+ * Returns the alpha of the variant.
+ *
+ * @return The alpha of the variant.
+ */
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ /**
+ * Sets the alpha of the variant.
+ *
+ * @param alpha The alpha value to set.
+ */
+ public void setAlpha(float alpha) {
+ mAlpha = alpha;
+ }
+
+ /**
+ * Sets the layer of the variant.
+ *
+ * @param layer The layer value to set.
+ */
+ public void setLayer(int layer) {
+ mLayer = layer;
+ }
+
+ /**
+ * Returns the bounds of the variant.
+ *
+ * @return The bounds of the variant.
+ */
+ public Rect getBounds() {
+ return mBounds;
+ }
+
+ /**
+ * Sets the bounds of the variant.
+ *
+ * @param bounds The bounds to set.
+ */
+ public void setBounds(Rect bounds) {
+ mBounds = bounds;
+ }
+
+ /**
+ * Creates a Variant object from an XML parser.
+ *
+ * <p>This method parses an XML element with the tag "Variant" and extracts its attributes
+ * and child elements to create a Variant object.
+ *
+ * @param context The application context.
+ * @param panelState The panel data associated with this variant.
+ * @param parser The XML parser.
+ * @return A Variant object with the parsed properties.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ static Variant create(Context context, PanelState panelState, XmlPullParser parser) throws
+ XmlPullParserException, IOException {
+ parser.require(XmlPullParser.START_TAG, null, VARIANT_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ String id = attrs.getAttributeValue(null, ID_ATTRIBUTE);
+ String parentStr = attrs.getAttributeValue(null, PARENT_ATTRIBUTE);
+ Variant parent = panelState.getVariant(parentStr);
+ Variant result = new Variant(id, parent);
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) continue;
+ String name = parser.getName();
+ switch (name) {
+ case Visibility.VISIBILITY_TAG:
+ result.setVisibility(Visibility.create(parser).isVisible());
+ break;
+ case Alpha.ALPHA_TAG:
+ result.setAlpha(Alpha.create(parser).getAlpha());
+ break;
+ case Layer.LAYER_TAG:
+ result.setLayer(Layer.create(parser).getLayer());
+ break;
+ case Bounds.BOUNDS_TAG:
+ result.setBounds(Bounds.create(context, parser).getRect());
+ break;
+ default:
+ XmlPullParserHelper.skip(parser);
+ break;
+ }
+ }
+ return result;
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/Visibility.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Visibility.java
new file mode 100644
index 0000000..06ac811
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/Visibility.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Represents the visibility of a Panel in the Scalable UI system.
+ *
+ * <p>This class encapsulates a boolean value indicating whether a panel is visible or not.
+ * It can be created from an XML definition or directly using a boolean value.
+ */
+public class Visibility {
+ static final String VISIBILITY_TAG = "Visibility";
+ private static final String IS_VISIBLE_ATTRIBUTE = "isVisible";
+ static final boolean DEFAULT_VISIBILITY = true;
+
+ private final boolean mIsVisible;
+
+ /**
+ * Constructor for Visibility.
+ *
+ * @param isVisible Whether the element is visible.
+ */
+ public Visibility(boolean isVisible) {
+ this.mIsVisible = isVisible;
+ }
+
+ /**
+ * Copy constructor for Visibility.
+ *
+ * @param visibility The Visibility object to copy from.
+ */
+ public Visibility(Visibility visibility) {
+ this(visibility.mIsVisible);
+ }
+
+ /**
+ * Returns whether the element is visible.
+ *
+ * @return True if the element is visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return mIsVisible;
+ }
+
+ /**
+ * Creates a Visibility object from an XML parser.
+ *
+ * @param parser The XML parser.
+ * @return The created Visibility object.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ public static Visibility create(XmlPullParser parser) throws XmlPullParserException,
+ IOException {
+ parser.require(XmlPullParser.START_TAG, null, VISIBILITY_TAG);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ boolean isVisible = attrs.getAttributeBooleanValue(null, IS_VISIBLE_ATTRIBUTE,
+ DEFAULT_VISIBILITY);
+ parser.nextTag();
+ parser.require(XmlPullParser.END_TAG, null, VISIBILITY_TAG);
+ return new Visibility(isVisible);
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/model/XmlPullParserHelper.java b/car-scalable-ui-lib/src/com/android/car/scalableui/model/XmlPullParserHelper.java
new file mode 100644
index 0000000..a1141e5
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/model/XmlPullParserHelper.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.model;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * This class provides helper methods for working with XmlPullParser.
+ */
+public class XmlPullParserHelper {
+ /**
+ * Skips an XML tag and all its contents.
+ *
+ * @param parser The XML parser.
+ * @throws XmlPullParserException If an error occurs during XML parsing.
+ * @throws IOException If an I/O error occurs while reading the XML.
+ */
+ static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
+ if (parser.getEventType() != XmlPullParser.START_TAG) throw new IllegalStateException();
+ int depth = 1;
+ while (depth != 0) {
+ switch (parser.next()) {
+ case XmlPullParser.END_TAG:
+ depth--;
+ break;
+ case XmlPullParser.START_TAG:
+ depth++;
+ break;
+ }
+ }
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/panel/Panel.java b/car-scalable-ui-lib/src/com/android/car/scalableui/panel/Panel.java
new file mode 100644
index 0000000..38aace1
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/panel/Panel.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.panel;
+
+import android.graphics.Rect;
+
+/**
+ * Represents a rectangular panel that can be displayed on the screen.
+ * Panels have properties such as bounds, layer, visibility, and alpha.
+ */
+public interface Panel {
+ /**
+ * Gets the bounding rectangle of this panel.
+ *
+ * @return The bounding rectangle.
+ */
+ Rect getBounds();
+
+ /**
+ * Sets the bounding rectangle of this panel.
+ *
+ * @param bounds The new bounding rectangle.
+ */
+ void setBounds(Rect bounds);
+
+ /**
+ * Gets the layer of this panel.
+ * Panels with higher layer values are drawn on top of panels with lower layer values.
+ *
+ * @return The layer of this panel.
+ */
+ int getLayer();
+
+ /**
+ * Sets the layer of this panel.
+ *
+ * @param layer The new layer of this panel.
+ */
+ void setLayer(int layer);
+
+ /**
+ * Gets the x-coordinate of the left edge of this panel.
+ *
+ * @return The x-coordinate of the left edge.
+ */
+ int getX1();
+
+ /**
+ * Sets the x-coordinate of the left edge of this panel.
+ *
+ * @param x The new x-coordinate of the left edge.
+ */
+ void setX1(int x);
+
+ /**
+ * Gets the x-coordinate of the right edge of this panel.
+ *
+ * @return The x-coordinate of the right edge.
+ */
+ int getX2();
+
+ /**
+ * Sets the x-coordinate of the right edge of this panel.
+ *
+ * @param x The new x-coordinate of the right edge.
+ */
+ void setX2(int x);
+
+ /**
+ * Gets the y-coordinate of the top edge of this panel.
+ *
+ * @return The y-coordinate of the top edge.
+ */
+ int getY1();
+
+ /**
+ * Sets the y-coordinate of the top edge of this panel.
+ *
+ * @param y The new y-coordinate of the top edge.
+ */
+ void setY1(int y);
+
+ /**
+ * Gets the y-coordinate of the bottom edge of this panel.
+ *
+ * @return The y-coordinate of the bottom edge.
+ */
+ int getY2();
+
+ /**
+ * Sets the y-coordinate of the bottom edge of this panel.
+ *
+ * @param y The new y-coordinate of the bottom edge.
+ */
+ void setY2(int y);
+
+ /**
+ * Gets the alpha value of this panel.
+ * The alpha value is a float between 0.0 (fully transparent) and 1.0 (fully opaque).
+ *
+ * @return The alpha value of this panel.
+ */
+ float getAlpha();
+
+ /**
+ * Sets the alpha value of this panel.
+ *
+ * @param alpha The new alpha value.
+ */
+ void setAlpha(float alpha);
+
+ /**
+ * Sets the visibility of this panel.
+ *
+ * @param isVisible True if the panel should be visible, false otherwise.
+ */
+ void setVisibility(boolean isVisible);
+
+ /**
+ * Checks if this panel is visible.
+ *
+ * @return True if the panel is visible, false otherwise.
+ */
+ boolean isVisible();
+
+ /**
+ * Sets the role of this panel.
+ * The role of a panel can be used to identify its purpose or function.
+ *
+ * @param role The new role of this panel.
+ */
+ void setRole(int role);
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/panel/PanelPool.java b/car-scalable-ui-lib/src/com/android/car/scalableui/panel/PanelPool.java
new file mode 100644
index 0000000..aa643fe
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/panel/PanelPool.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.panel;
+
+import java.util.HashMap;
+
+/**
+ * A pool for managing {@link Panel} instances.
+ *
+ * <p>This class provides a centralized mechanism for creating and retrieving panels, ensuring that
+ * only one instance of a panel with a given ID exists at a time. It uses a
+ * {@link PanelCreatorDelegate} to handle the actual creation of panel instances.
+ */
+public class PanelPool {
+ private static final PanelPool sInstance = new PanelPool();
+
+ /**
+ * An instance of the {@link PanelPool}.
+ */
+ public static PanelPool getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * A delegate interface for creating {@link Panel} instances.
+ */
+ public interface PanelCreatorDelegate {
+ /**
+ * Creates a panel object.
+ * @param id given identifier for the panel.
+ * @return the panel object.
+ */
+ Panel createPanel(String id);
+ }
+
+ private final HashMap<String, Panel> mPanels = new HashMap<>();
+ private PanelCreatorDelegate mDelegate;
+
+ private PanelPool() {}
+
+ /**
+ * Sets the {@link PanelCreatorDelegate} to be used for creating panel instances.
+ *
+ * @param delegate The delegate to set.
+ */
+ public void setDelegate(PanelCreatorDelegate delegate) {
+ mDelegate = delegate;
+ }
+
+ /**
+ * Clears all panels from the pool.
+ */
+ public void clearPanels() {
+ mPanels.clear();
+ }
+
+ /**
+ * Retrieves a panel with the given ID.
+ *
+ * <p>If a panel with the given ID already exists in the pool, it is returned. Otherwise, a new
+ * panel is created using the {@link PanelCreatorDelegate}, added to the pool, and returned.
+ *
+ * @param id The ID of the panel to retrieve.
+ * @return The panel with the given ID.
+ */
+ public Panel getPanel(String id) {
+ Panel panel = mPanels.get(id);
+ if (panel == null) {
+ panel = mDelegate.createPanel(id);
+ mPanels.put(id, panel);
+ }
+ return mPanels.get(id);
+ }
+}
diff --git a/car-scalable-ui-lib/src/com/android/car/scalableui/panel/PanelView.java b/car-scalable-ui-lib/src/com/android/car/scalableui/panel/PanelView.java
new file mode 100644
index 0000000..6ef93ae
--- /dev/null
+++ b/car-scalable-ui-lib/src/com/android/car/scalableui/panel/PanelView.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2024 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.car.scalableui.panel;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A view based implementation of a {@link Panel}.
+ */
+public class PanelView extends FrameLayout implements Panel {
+ private static final String DRAWABLE_RESOURCE_TYPE = "drawable";
+ private static final String LAYOUT_RESOURCE_TYPE = "layout";
+
+ private int mLayer = -1;
+ private int mRole = 0;
+
+ private int mImageHolderLayoutId;
+ private int mImageId;
+
+ public PanelView(@NonNull Context context) {
+ super(context);
+ }
+
+ public PanelView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PanelView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public Rect getBounds() {
+ return new Rect(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ @Override
+ public void setBounds(Rect bounds) {
+ LayoutParams params = new LayoutParams(bounds.width(), bounds.height());
+ params.topMargin = bounds.top;
+ params.leftMargin = bounds.left;
+ setLayoutParams(params);
+
+ // Update left, right, top and bottom to make sure these values are correctly set before a
+ // full round of re-layout.
+ setLeft(bounds.left);
+ setRight(bounds.right);
+ setTop(bounds.top);
+ setBottom(bounds.bottom);
+ }
+
+ public int getLayer() {
+ return mLayer;
+ }
+
+ /**
+ * Sets the z-order of the panel.
+ * @param layer the required z-order.
+ */
+ public void setLayer(int layer) {
+ if (this.mLayer == layer) return;
+ this.mLayer = layer;
+ ViewGroup parent = (ViewGroup) getParent();
+ boolean isSorted = false;
+ // Make sure all the sibling PanelViews have the correct relative z-order.
+ while (!isSorted) {
+ isSorted = true;
+ int lastLayer = Integer.MAX_VALUE;
+ for (int i = parent.getChildCount() - 1; i >= 0; i--) {
+ View child = parent.getChildAt(i);
+ if (!(child instanceof PanelView panelView)) continue;
+ if (panelView.getLayer() > lastLayer) {
+ panelView.bringToFront();
+ isSorted = false;
+ break;
+ }
+ lastLayer = panelView.getLayer();
+ }
+ }
+ }
+
+ public int getX1() {
+ return getLeft();
+ }
+
+ public int getX2() {
+ return getRight();
+ }
+
+ public int getY1() {
+ return getTop();
+ }
+
+ public int getY2() {
+ return getBottom();
+ }
+
+ @Override
+ public void setX1(int x) {
+ setBounds(new Rect(x, getTop(), getRight(), getBottom()));
+ }
+
+ @Override
+ public void setX2(int x) {
+ setBounds(new Rect(getLeft(), getTop(), x, getBottom()));
+ }
+
+ @Override
+ public void setY1(int y) {
+ setBounds(new Rect(getLeft(), y, getRight(), getBottom()));
+ }
+
+ @Override
+ public void setY2(int y) {
+ setBounds(new Rect(getLeft(), getTop(), getRight(), y));
+ }
+
+ /**
+ * Checks if this panel is visible.
+ *
+ * @return True if the panel is visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return super.getVisibility() == VISIBLE;
+ }
+
+ /**
+ * Sets the visibility of this panel.
+ *
+ * @param isVisible True if the panel should be visible, false otherwise.
+ */
+ public void setVisibility(boolean isVisible) {
+ super.setVisibility(isVisible ? VISIBLE : INVISIBLE);
+ }
+
+ /**
+ * Gets the alpha value of this panel.
+ * The alpha value is a float between 0.0 (fully transparent) and 1.0 (fully opaque).
+ *
+ * @return The alpha value of this panel.
+ */
+ public float getAlpha() {
+ return super.getAlpha();
+ }
+
+ /**
+ * Sets the alpha value of this panel.
+ *
+ * @param alpha The new alpha value.
+ */
+ public void setAlpha(float alpha) {
+ super.setAlpha(alpha);
+ }
+
+ public void setImageHolderLayoutId(int imageHolderLayoutId) {
+ this.mImageHolderLayoutId = imageHolderLayoutId;
+ }
+
+ public void setImageId(int imageId) {
+ this.mImageId = imageId;
+ }
+
+ /**
+ * Sets the role of this panel.
+ * The role of a panel can be used to identify its purpose or function.
+ *
+ * @param role The new role of this panel.
+ */
+ public void setRole(int role) {
+ if (this.mRole == role) return;
+ this.mRole = role;
+ if (isDrawableRole(role)) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ View view = inflater.inflate(mImageHolderLayoutId, this, true);
+ ImageView imageView = view.findViewById(mImageId);
+ if (imageView != null) {
+ imageView.setImageResource(role);
+ }
+ } else if (isLayoutRole(role)) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ inflater.inflate(role, this, true);
+ } else {
+ throw new UnsupportedOperationException("Specified role is not supported");
+ }
+ }
+
+ private boolean isDrawableRole(int role) {
+ String resourceTypeName = getContext().getResources().getResourceTypeName(role);
+ return DRAWABLE_RESOURCE_TYPE.equals(resourceTypeName);
+ }
+
+ private boolean isLayoutRole(int role) {
+ String resourceTypeName = getContext().getResources().getResourceTypeName(role);
+ return LAYOUT_RESOURCE_TYPE.equals(resourceTypeName);
+ }
+}