| /* |
| * Copyright (C) 2017 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 androidx.car.widget; |
| |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.drawable.Drawable; |
| import androidx.annotation.DrawableRes; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.interpolator.view.animation.FastOutSlowInInterpolator; |
| import android.transition.ChangeBounds; |
| import android.transition.Fade; |
| import android.transition.TransitionManager; |
| import android.transition.TransitionSet; |
| import android.util.AttributeSet; |
| import android.util.SparseArray; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| import android.widget.ImageButton; |
| import android.widget.LinearLayout; |
| import android.widget.RelativeLayout; |
| import android.widget.Space; |
| |
| import java.lang.annotation.Retention; |
| import java.util.Locale; |
| |
| import androidx.car.R; |
| |
| /** |
| * An actions panel with three distinctive zones: |
| * <ul> |
| * <li>Main control: located in the bottom center it shows a highlighted icon and a circular |
| * progress bar. |
| * <li>Secondary controls: these are displayed at the left and at the right of the main control. |
| * <li>Overflow controls: these are displayed at the left and at the right of the secondary controls |
| * (if the space allows) and on the additional space if the panel is expanded. |
| * </ul> |
| */ |
| public class ActionBar extends RelativeLayout { |
| private static final String TAG = "ActionBar"; |
| |
| // ActionBar container |
| private ViewGroup mActionBarWrapper; |
| // Rows container |
| private ViewGroup mRowsContainer; |
| // All slots in this action bar where 0 is the bottom-start corner of the matrix, and |
| // mNumColumns * nNumRows - 1 is the top-end corner |
| private FrameLayout[] mSlots; |
| /** Views to set in particular {@link SlotPosition}s */ |
| private final SparseArray<View> mFixedViews = new SparseArray<>(); |
| // View to be used for the expand/collapse action |
| private @Nullable View mExpandCollapseView; |
| // Default expand/collapse view to use one is not provided. |
| private View mDefaultExpandCollapseView; |
| // Number of rows in actual use. This is the number of extra rows that will be displayed when |
| // the action bar is expanded |
| private int mNumExtraRowsInUse; |
| // Whether the action bar is expanded or not. |
| private boolean mIsExpanded; |
| // Views to accomodate in the slots. |
| private @Nullable View[] mViews; |
| // Number of columns of slots to use. |
| private int mNumColumns; |
| // Maximum number of rows to use. |
| private int mNumRows; |
| |
| @Retention(SOURCE) |
| @IntDef({SLOT_MAIN, SLOT_LEFT, SLOT_RIGHT, SLOT_EXPAND_COLLAPSE}) |
| public @interface SlotPosition {} |
| |
| /** Slot used for main actions {@link ActionBar}, usually at the bottom center */ |
| public static final int SLOT_MAIN = 0; |
| /** Slot used to host 'move left', 'rewind', 'previous' or similar secondary actions, |
| * usually at the left of the main action on the bottom row */ |
| public static final int SLOT_LEFT = 1; |
| /** Slot used to host 'move right', 'fast-forward', 'next' or similar secondary actions, |
| * usually at the right of the main action on the bottom row */ |
| public static final int SLOT_RIGHT = 2; |
| /** Slot reserved for the expand/collapse button */ |
| public static final int SLOT_EXPAND_COLLAPSE = 3; |
| |
| // Minimum number of columns supported |
| private static final int MIN_COLUMNS = 3; |
| // Weight for the spacers used at the start and end of each slots row. |
| private static final float SPACERS_WEIGHT = 0.5f; |
| |
| public ActionBar(Context context) { |
| super(context); |
| init(context, null, 0, 0); |
| } |
| |
| public ActionBar(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| init(context, attrs, 0, 0); |
| } |
| |
| public ActionBar(Context context, AttributeSet attrs, int defStyleAttrs) { |
| super(context, attrs, defStyleAttrs); |
| init(context, attrs, defStyleAttrs, 0); |
| } |
| |
| public ActionBar(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { |
| super(context, attrs, defStyleAttrs, defStyleRes); |
| init(context, attrs, defStyleAttrs, defStyleRes); |
| } |
| |
| private void init(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { |
| inflate(context, R.layout.action_bar, this); |
| |
| TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ActionBar, |
| defStyleAttrs, defStyleRes); |
| mNumColumns = Math.max(ta.getInteger(R.styleable.ActionBar_columns, MIN_COLUMNS), |
| MIN_COLUMNS); |
| ta.recycle(); |
| |
| mActionBarWrapper = findViewById(R.id.action_bar_wrapper); |
| mRowsContainer = findViewById(R.id.rows_container); |
| mNumRows = mRowsContainer.getChildCount(); |
| mSlots = new FrameLayout[mNumColumns * mNumRows]; |
| |
| for (int i = 0; i < mNumRows; i++) { |
| // Slots are reserved in reverse order (first slots are in the bottom row) |
| ViewGroup mRow = (ViewGroup) mRowsContainer.getChildAt(mNumRows - i - 1); |
| // Inflate space on the left |
| Space space = new Space(context); |
| mRow.addView(space); |
| space.setLayoutParams(new LinearLayout.LayoutParams(0, |
| ViewGroup.LayoutParams.MATCH_PARENT, SPACERS_WEIGHT)); |
| // Inflate necessary number of columns |
| for (int j = 0; j < mNumColumns; j++) { |
| int pos = i * mNumColumns + j; |
| mSlots[pos] = (FrameLayout) inflate(context, R.layout.action_bar_slot, null); |
| mSlots[pos].setLayoutParams(new LinearLayout.LayoutParams(0, |
| ViewGroup.LayoutParams.MATCH_PARENT, 1f)); |
| mRow.addView(mSlots[pos]); |
| } |
| // Inflate space on the right |
| space = new Space(context); |
| mRow.addView(space); |
| space.setLayoutParams(new LinearLayout.LayoutParams(0, |
| ViewGroup.LayoutParams.MATCH_PARENT, SPACERS_WEIGHT)); |
| } |
| |
| mDefaultExpandCollapseView = createIconButton(context, R.drawable.ic_overflow); |
| mDefaultExpandCollapseView.setContentDescription(context.getString( |
| R.string.action_bar_expand_collapse_button)); |
| mDefaultExpandCollapseView.setOnClickListener(v -> onExpandCollapse()); |
| } |
| |
| /** |
| * Returns an index in the {@link #mSlots} array, given a well-known slot position. |
| */ |
| private int getSlotIndex(@SlotPosition int slotPosition) { |
| switch (slotPosition) { |
| case SLOT_MAIN: |
| return mNumColumns / 2; |
| case SLOT_LEFT: |
| return mNumColumns < 3 ? -1 : (mNumColumns / 2) - 1; |
| case SLOT_RIGHT: |
| return mNumColumns < 2 ? -1 : (mNumColumns / 2) + 1; |
| case SLOT_EXPAND_COLLAPSE: |
| return mNumColumns - 1; |
| default: |
| throw new IllegalArgumentException("Unknown position: " + slotPosition); |
| } |
| } |
| |
| /** |
| * Sets or clears the view to be displayed at a particular position. |
| * |
| * @param view view to be displayed, or null to leave the position available. |
| * @param slotPosition position to update |
| */ |
| public void setView(@Nullable View view, @SlotPosition int slotPosition) { |
| if (view != null) { |
| mFixedViews.put(slotPosition, view); |
| } else { |
| mFixedViews.remove(slotPosition); |
| } |
| updateViewsLayout(); |
| } |
| |
| /** |
| * Sets the view to use for the expand/collapse action. If not provided, a default |
| * {@link ImageButton} will be used. The provided {@link View} should be able be able to display |
| * changes in the "activated" state appropriately. |
| * |
| * @param view {@link View} to use for the expand/collapse action. |
| */ |
| public void setExpandCollapseView(@NonNull View view) { |
| mExpandCollapseView = view; |
| mExpandCollapseView.setOnClickListener(v -> onExpandCollapse()); |
| updateViewsLayout(); |
| } |
| |
| private View getExpandCollapseView() { |
| return mExpandCollapseView != null ? mExpandCollapseView : mDefaultExpandCollapseView; |
| } |
| |
| private ImageButton createIconButton(Context context, @DrawableRes int iconResId) { |
| ImageButton button = (ImageButton) inflate(context, R.layout.action_bar_button, null); |
| Drawable icon = context.getDrawable(iconResId); |
| button.setImageDrawable(icon); |
| return button; |
| } |
| |
| /** |
| * Sets the views to include in each available slot of the action bar. Slots will be filled from |
| * start to end (i.e: left to right) and from bottom to top. If more views than available slots |
| * are provided, all extra views will be ignored. |
| * |
| * @param views array of views to include in each available slot. |
| */ |
| public void setViews(@Nullable View[] views) { |
| mViews = views; |
| updateViewsLayout(); |
| } |
| |
| private void updateViewsLayout() { |
| // Prepare an array of positions taken |
| int totalSlots = mSlots.length; |
| View[] slotViews = new View[totalSlots]; |
| |
| // Take all known positions |
| for (int i = 0; i < mFixedViews.size(); i++) { |
| int index = getSlotIndex(mFixedViews.keyAt(i)); |
| if (index >= 0 && index < slotViews.length) { |
| slotViews[index] = mFixedViews.valueAt(i); |
| } |
| } |
| |
| // Set all views using both the fixed and flexible positions |
| int expandCollapseIndex = getSlotIndex(SLOT_EXPAND_COLLAPSE); |
| int lastUsedIndex = 0; |
| int viewsIndex = 0; |
| for (int i = 0; i < totalSlots; i++) { |
| View viewToUse = null; |
| |
| if (slotViews[i] != null) { |
| // If there is a view assigned for this slot, use it. |
| viewToUse = slotViews[i]; |
| } else if (i == expandCollapseIndex && mViews != null |
| && viewsIndex < mViews.length - 1) { |
| // If this is the expand/collapse slot, use the corresponding view |
| viewToUse = getExpandCollapseView(); |
| } else if (mViews != null && viewsIndex < mViews.length) { |
| // Otherwise, if the slot is not reserved, and if we still have views to assign, |
| // take one and assign it to this slot. |
| viewToUse = mViews[viewsIndex]; |
| viewsIndex++; |
| } |
| setView(viewToUse, mSlots[i]); |
| if (viewToUse != null) { |
| lastUsedIndex = i; |
| } |
| } |
| |
| mNumExtraRowsInUse = lastUsedIndex / mNumColumns; |
| } |
| |
| private void setView(@Nullable View view, FrameLayout container) { |
| container.removeAllViews(); |
| if (view != null) { |
| container.addView(view); |
| container.setVisibility(VISIBLE); |
| view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); |
| } else { |
| container.setVisibility(INVISIBLE); |
| } |
| } |
| |
| private void onExpandCollapse() { |
| mIsExpanded = !mIsExpanded; |
| mSlots[getSlotIndex(SLOT_EXPAND_COLLAPSE)].setActivated(mIsExpanded); |
| |
| int animationDuration = getContext().getResources().getInteger(mIsExpanded |
| ? R.integer.car_action_bar_expand_anim_duration |
| : R.integer.car_action_bar_collapse_anim_duration); |
| TransitionSet set = new TransitionSet() |
| .addTransition(new ChangeBounds()) |
| .addTransition(new Fade()) |
| .setDuration(animationDuration) |
| .setInterpolator(new FastOutSlowInInterpolator()); |
| TransitionManager.beginDelayedTransition(mActionBarWrapper, set); |
| for (int i = 0; i < mNumExtraRowsInUse; i++) { |
| mRowsContainer.getChildAt(i).setVisibility(mIsExpanded ? View.VISIBLE : View.GONE); |
| } |
| } |
| |
| /** |
| * Returns the view assigned to the given row and column, after layout. |
| * |
| * @param rowIdx row index from 0 being the top row, and {@link #mNumRows{ -1 being the bottom |
| * row. |
| * @param colIdx column index from 0 on start (left), to {@link #mNumColumns} on end (right) |
| */ |
| @VisibleForTesting |
| @Nullable |
| View getViewAt(int rowIdx, int colIdx) { |
| if (rowIdx < 0 || rowIdx > mRowsContainer.getChildCount()) { |
| throw new IllegalArgumentException(String.format((Locale) null, |
| "Row index out of range (requested: %d, max: %d)", |
| rowIdx, mRowsContainer.getChildCount())); |
| } |
| if (colIdx < 0 || colIdx > mNumColumns) { |
| throw new IllegalArgumentException(String.format((Locale) null, |
| "Column index out of range (requested: %d, max: %d)", |
| colIdx, mNumColumns)); |
| } |
| FrameLayout slot = (FrameLayout) ((LinearLayout) mRowsContainer.getChildAt(rowIdx)) |
| .getChildAt(colIdx + 1); |
| return slot.getChildCount() > 0 ? slot.getChildAt(0) : null; |
| } |
| } |