summaryrefslogtreecommitdiff
path: root/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java
diff options
context:
space:
mode:
Diffstat (limited to 'Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java')
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java381
1 files changed, 381 insertions, 0 deletions
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java
new file mode 100644
index 0000000..fdeb929
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2021 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.libraries.templates.host.view.widgets.common;
+
+import static androidx.car.app.model.Action.TYPE_BACK;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.Toast;
+import androidx.annotation.ColorInt;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.OnClickDelegate;
+import com.android.car.libraries.apphost.common.CommonUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.view.common.ActionButtonListParams;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Displays an {@link Action} as a button. */
+public class ActionButtonView extends FrameLayout {
+ private static final int[] BUTTON_PRIMARY =
+ new int[] {R.attr.type_primary};
+ private static final int[] BUTTON_CUSTOM =
+ new int[] {R.attr.type_custom};
+ private static final int[] BUTTON_CUSTOM_PRIMARY =
+ new int[] {
+ R.attr.type_custom,
+ R.attr.type_primary
+ };
+
+ @IntDef(
+ flag = true,
+ value = {
+ FLAG_SUPPORT_REORDERING_BY_OEM,
+ FLAG_SUPPORT_CUSTOMIZED_COLOR_BY_OEM,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActionFlag {}
+
+ public static final int FLAG_SUPPORT_REORDERING_BY_OEM = 1 << 0;
+ public static final int FLAG_SUPPORT_CUSTOMIZED_COLOR_BY_OEM = 1 << 1;
+
+ @ColorInt private final int mDefaultBackgroundColor;
+ @ColorInt private final int mDefaultIconTint;
+ private final int mMinWidthWithText;
+ private final int mMinWidthWithoutText;
+ private final int mSideAlignmentSpacing;
+ private final int mCustomMaxEms;
+ private boolean mIsPrimary;
+ private boolean mIsCustom;
+ private boolean mIsEnabled;
+ private String mDisabledToastMessage;
+
+ /**
+ * The content alignment.
+ *
+ * <p>The possible values are:
+ *
+ * <ul>
+ * <li>0: center (default)
+ * <li>1: left
+ * <li>2: right
+ * </ul>
+ */
+ private final int mContentAlignment;
+
+ /** A flag that indicates whether the buttons stretch horizontally to fill the available space. */
+ private final boolean mButtonsStretch;
+
+ public ActionButtonView(Context context) {
+ this(context, null);
+ }
+
+ public ActionButtonView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ActionButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ActionButtonView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateActionButtonDefaultBackgroundColor,
+ R.attr.templateActionDefaultIconTint,
+ R.attr.templateActionWithTextMinWidth,
+ R.attr.templateActionWithoutTextMinWidth,
+ R.attr.templateActionButtonSideAlignmentSpacing,
+ R.attr.templateActionButtonListButtonContentAlignment,
+ R.attr.templateActionButtonListButtonStretchHorizontal,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mDefaultBackgroundColor = ta.getColor(0, 0);
+ mDefaultIconTint = ta.getColor(1, 0);
+ mMinWidthWithText = ta.getDimensionPixelSize(2, 0);
+ mMinWidthWithoutText = ta.getDimensionPixelSize(3, 0);
+ mSideAlignmentSpacing = ta.getDimensionPixelSize(4, 0);
+ mContentAlignment = ta.getInteger(5, 0);
+ mButtonsStretch = ta.getBoolean(6, false);
+ ta.recycle();
+
+ // TODO(b/184195457): remove the custom maxEms limit when we remove the limit for all
+ ta =
+ context.obtainStyledAttributes(
+ attrs, R.styleable.ActionButtonView, defStyleAttr, defStyleRes);
+ mCustomMaxEms = ta.getInt(R.styleable.ActionButtonView_textMaxEms, 0);
+ ta.recycle();
+
+ mIsEnabled = true;
+ }
+
+ /** Returns the {@link android.view.View} title for testing. */
+ @Nullable
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public String getTitle() {
+ CarUiTextView carUiTextView = findViewById(R.id.action_text);
+ if (carUiTextView == null) {
+ return null;
+ }
+ return carUiTextView.getText().toString();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ }
+
+ /** Updates the view from the {@link Action} model. */
+ public ActionButtonView setAction(
+ TemplateContext templateContext, Action action, ActionButtonListParams params) {
+ L.v(LogTags.TEMPLATE, "Setting action view with action: %s", action);
+
+ removeAllViews();
+
+ final boolean allowAppColor = params.allowAppColor();
+
+ // Set the background color
+ final CarColor color = action.getBackgroundColor();
+ @ColorInt int appBackgroundColor = mDefaultBackgroundColor;
+ if (color != null && allowAppColor) {
+ appBackgroundColor =
+ ActionButtonViewUtils.getBackgroundColor(
+ templateContext,
+ action,
+ /* surroundingColor= */ params.getSurroundingColor(),
+ /* defaultBackgroundColor= */ mDefaultBackgroundColor);
+ }
+
+ final boolean useAppColors = appBackgroundColor != mDefaultBackgroundColor;
+ if (useAppColors) {
+ // Set the background as tint to not override the round-corner drawable with ripple effects.
+ setBackgroundTintList(ColorStateList.valueOf(appBackgroundColor));
+ }
+
+ boolean useOemColor =
+ templateContext.getCarHostConfig().isButtonColorOverriddenByOEM()
+ && params.allowOemColorOverride();
+ updateState(
+ /* isCustom= */ useAppColors,
+ /* isPrimary= */ useOemColor && ActionButtonViewUtils.isPrimaryAction(action));
+
+ // Check if the title's color span has enough contrast against the background color
+ final CarColorConstraints textColorConstraints;
+ CarText titleText = action.getTitle();
+ if (allowAppColor
+ && titleText != null
+ && CarTextUtils.checkColorContrast(templateContext, titleText, appBackgroundColor)) {
+ textColorConstraints = CarColorConstraints.UNCONSTRAINED;
+ } else {
+ textColorConstraints = CarColorConstraints.NO_COLOR;
+ }
+
+ final CarTextParams carTextParams =
+ CarTextParams.builder()
+ .setColorSpanConstraints(textColorConstraints)
+ .setBackgroundColor(appBackgroundColor)
+ .build();
+ CharSequence title =
+ CarTextUtils.toCharSequenceOrEmpty(templateContext, titleText, carTextParams);
+ CarIcon icon = ImageUtils.getIconFromAction(action);
+
+ boolean hasTitle = title.length() > 0;
+
+ setMinimumWidth(hasTitle ? mMinWidthWithText : mMinWidthWithoutText);
+
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ if (icon != null && hasTitle) {
+ inflater.inflate(R.layout.action_button_view_icon_text, this);
+ } else if (hasTitle) {
+ inflater.inflate(R.layout.action_button_view_text, this);
+ } else {
+ inflater.inflate(R.layout.action_button_view_icon, this);
+ }
+
+ if (icon != null) {
+ ImageView iconView = findViewById(R.id.action_icon);
+ ImageViewParams imageViewParams =
+ ImageViewParams.builder()
+ .setDefaultTint(mDefaultIconTint)
+ .setForceTinting(true)
+ .setIgnoreAppTint(!allowAppColor)
+ .setBackgroundColor(appBackgroundColor)
+ .build();
+ ImageUtils.setImageSrc(templateContext, icon, iconView, imageViewParams);
+ }
+
+ if (hasTitle) {
+ CarUiTextView carUiTextView = findViewById(R.id.action_text);
+ if (mCustomMaxEms > 0) {
+ carUiTextView.setMaxEms(mCustomMaxEms);
+ } else if (mButtonsStretch) {
+ // If max EMS is not set and the button stretches, allow the buttons to fill all
+ // available space.
+ carUiTextView.setMaxWidth(Integer.MAX_VALUE);
+ }
+
+ carUiTextView.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext, action.getTitle(), carTextParams, carUiTextView.getMaxLines()));
+ }
+
+ // Update the click listener, if one is set.
+ if (action.getType() == TYPE_BACK) {
+ setOnClickListener(
+ v -> {
+ if (!mIsEnabled) {
+ showDisabledToast(templateContext);
+ return;
+ }
+
+ templateContext.getBackPressedHandler().onBackPressed();
+ });
+ } else {
+ OnClickDelegate onClickDelegate = action.getOnClickDelegate();
+ if (onClickDelegate != null) {
+ setOnClickListener(
+ v -> {
+ ViewUtils.logCarAppTelemetry(templateContext, UiAction.ACTION_BUTTON_CLICKED);
+
+ if (!mIsEnabled) {
+ showDisabledToast(templateContext);
+ return;
+ }
+
+ CommonUtils.dispatchClick(templateContext, onClickDelegate);
+ });
+ } else {
+ setOnClickListener(null);
+ }
+ }
+
+ // Set the content alignment and margins
+ View contentView = getChildAt(0);
+ if (contentView != null) {
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) contentView.getLayoutParams();
+ int contentGravity = getContentGravity(mContentAlignment);
+ layoutParams.gravity = contentGravity;
+
+ // If the content is aligned to the side, use side-alignment-specific horizontal
+ // margins.
+ if (contentGravity != Gravity.CENTER) {
+ layoutParams.leftMargin = mSideAlignmentSpacing;
+ layoutParams.rightMargin = mSideAlignmentSpacing;
+ }
+
+ contentView.setLayoutParams(layoutParams);
+ }
+
+ return this;
+ }
+
+ /** {@link ActionButtonView} will carry out the action when clicked on without toast. */
+ public void enableActionButton() {
+ mIsEnabled = true;
+ }
+
+ /**
+ * {@link ActionButtonView} will show a toast with given message instead of carrying out the
+ * action when clicked on.
+ */
+ public void disableActionButton(String disabledToastMessage) {
+ mIsEnabled = false;
+ mDisabledToastMessage = disabledToastMessage;
+ }
+
+ private void showDisabledToast(TemplateContext templateContext) {
+ templateContext.getToastController().showToast(mDisabledToastMessage, Toast.LENGTH_SHORT);
+ }
+
+ /** Returns {@code true} if action button is enabled. */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public boolean isActionButtonEnabled() {
+ return mIsEnabled;
+ }
+
+ /** Gets the gravity value that corresponds to the content alignment value. */
+ private static int getContentGravity(int contentAlignment) {
+ int gravity = Gravity.CENTER_VERTICAL;
+ switch (contentAlignment) {
+ case 1:
+ gravity |= Gravity.LEFT;
+ break;
+ case 2:
+ gravity |= Gravity.RIGHT;
+ break;
+ case 0: // fall-through
+ default:
+ gravity = Gravity.CENTER;
+ }
+
+ return gravity;
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ int[] additionalStates;
+ if (mIsPrimary && mIsCustom) {
+ additionalStates = BUTTON_CUSTOM_PRIMARY;
+ } else if (mIsPrimary) {
+ additionalStates = BUTTON_PRIMARY;
+ } else if (mIsCustom) {
+ additionalStates = BUTTON_CUSTOM;
+ } else {
+ return super.onCreateDrawableState(extraSpace);
+ }
+ int[] state = super.onCreateDrawableState(extraSpace + additionalStates.length);
+ mergeDrawableStates(state, additionalStates);
+ return state;
+ }
+
+ private void updateState(boolean isCustom, boolean isPrimary) {
+ if (isCustom != mIsCustom || isPrimary != mIsPrimary) {
+ mIsCustom = isCustom;
+ mIsPrimary = isPrimary;
+ refreshDrawableState();
+ }
+ }
+}