Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.car.widget;
     18 
     19 import static java.lang.annotation.RetentionPolicy.SOURCE;
     20 
     21 import android.content.Context;
     22 import android.content.res.TypedArray;
     23 import android.graphics.drawable.Drawable;
     24 import androidx.annotation.DrawableRes;
     25 import androidx.annotation.IntDef;
     26 import androidx.annotation.NonNull;
     27 import androidx.annotation.Nullable;
     28 import androidx.annotation.VisibleForTesting;
     29 import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
     30 import android.transition.ChangeBounds;
     31 import android.transition.Fade;
     32 import android.transition.TransitionManager;
     33 import android.transition.TransitionSet;
     34 import android.util.AttributeSet;
     35 import android.util.SparseArray;
     36 import android.view.Gravity;
     37 import android.view.View;
     38 import android.view.ViewGroup;
     39 import android.widget.FrameLayout;
     40 import android.widget.ImageButton;
     41 import android.widget.LinearLayout;
     42 import android.widget.RelativeLayout;
     43 import android.widget.Space;
     44 
     45 import java.lang.annotation.Retention;
     46 import java.util.Locale;
     47 
     48 import androidx.car.R;
     49 
     50 /**
     51  * An actions panel with three distinctive zones:
     52  * <ul>
     53  * <li>Main control: located in the bottom center it shows a highlighted icon and a circular
     54  * progress bar.
     55  * <li>Secondary controls: these are displayed at the left and at the right of the main control.
     56  * <li>Overflow controls: these are displayed at the left and at the right of the secondary controls
     57  * (if the space allows) and on the additional space if the panel is expanded.
     58  * </ul>
     59  */
     60 public class ActionBar extends RelativeLayout {
     61     private static final String TAG = "ActionBar";
     62 
     63     // ActionBar container
     64     private ViewGroup mActionBarWrapper;
     65     // Rows container
     66     private ViewGroup mRowsContainer;
     67     // All slots in this action bar where 0 is the bottom-start corner of the matrix, and
     68     // mNumColumns * nNumRows - 1 is the top-end corner
     69     private FrameLayout[] mSlots;
     70     /** Views to set in particular {@link SlotPosition}s */
     71     private final SparseArray<View> mFixedViews = new SparseArray<>();
     72     // View to be used for the expand/collapse action
     73     private @Nullable View mExpandCollapseView;
     74     // Default expand/collapse view to use one is not provided.
     75     private View mDefaultExpandCollapseView;
     76     // Number of rows in actual use. This is the number of extra rows that will be displayed when
     77     // the action bar is expanded
     78     private int mNumExtraRowsInUse;
     79     // Whether the action bar is expanded or not.
     80     private boolean mIsExpanded;
     81     // Views to accomodate in the slots.
     82     private @Nullable View[] mViews;
     83     // Number of columns of slots to use.
     84     private int mNumColumns;
     85     // Maximum number of rows to use.
     86     private int mNumRows;
     87 
     88     @Retention(SOURCE)
     89     @IntDef({SLOT_MAIN, SLOT_LEFT, SLOT_RIGHT, SLOT_EXPAND_COLLAPSE})
     90     public @interface SlotPosition {}
     91 
     92     /** Slot used for main actions {@link ActionBar}, usually at the bottom center */
     93     public static final int SLOT_MAIN = 0;
     94     /** Slot used to host 'move left', 'rewind', 'previous' or similar secondary actions,
     95      * usually at the left of the main action on the bottom row */
     96     public static final int SLOT_LEFT = 1;
     97     /** Slot used to host 'move right', 'fast-forward', 'next' or similar secondary actions,
     98      * usually at the right of the main action on the bottom row */
     99     public static final int SLOT_RIGHT = 2;
    100     /** Slot reserved for the expand/collapse button */
    101     public static final int SLOT_EXPAND_COLLAPSE = 3;
    102 
    103     // Minimum number of columns supported
    104     private static final int MIN_COLUMNS = 3;
    105     // Weight for the spacers used at the start and end of each slots row.
    106     private static final float SPACERS_WEIGHT = 0.5f;
    107 
    108     public ActionBar(Context context) {
    109         super(context);
    110         init(context, null, 0, 0);
    111     }
    112 
    113     public ActionBar(Context context, AttributeSet attrs) {
    114         super(context, attrs);
    115         init(context, attrs, 0, 0);
    116     }
    117 
    118     public ActionBar(Context context, AttributeSet attrs, int defStyleAttrs) {
    119         super(context, attrs, defStyleAttrs);
    120         init(context, attrs, defStyleAttrs, 0);
    121     }
    122 
    123     public ActionBar(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
    124         super(context, attrs, defStyleAttrs, defStyleRes);
    125         init(context, attrs, defStyleAttrs, defStyleRes);
    126     }
    127 
    128     private void init(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
    129         inflate(context, R.layout.action_bar, this);
    130 
    131         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ActionBar,
    132                 defStyleAttrs, defStyleRes);
    133         mNumColumns = Math.max(ta.getInteger(R.styleable.ActionBar_columns, MIN_COLUMNS),
    134                 MIN_COLUMNS);
    135         ta.recycle();
    136 
    137         mActionBarWrapper = findViewById(R.id.action_bar_wrapper);
    138         mRowsContainer = findViewById(R.id.rows_container);
    139         mNumRows = mRowsContainer.getChildCount();
    140         mSlots = new FrameLayout[mNumColumns * mNumRows];
    141 
    142         for (int i = 0; i < mNumRows; i++) {
    143             // Slots are reserved in reverse order (first slots are in the bottom row)
    144             ViewGroup mRow = (ViewGroup) mRowsContainer.getChildAt(mNumRows - i - 1);
    145             // Inflate space on the left
    146             Space space = new Space(context);
    147             mRow.addView(space);
    148             space.setLayoutParams(new LinearLayout.LayoutParams(0,
    149                     ViewGroup.LayoutParams.MATCH_PARENT, SPACERS_WEIGHT));
    150             // Inflate necessary number of columns
    151             for (int j = 0; j < mNumColumns; j++) {
    152                 int pos = i * mNumColumns + j;
    153                 mSlots[pos] = (FrameLayout) inflate(context, R.layout.action_bar_slot, null);
    154                 mSlots[pos].setLayoutParams(new LinearLayout.LayoutParams(0,
    155                         ViewGroup.LayoutParams.MATCH_PARENT, 1f));
    156                 mRow.addView(mSlots[pos]);
    157             }
    158             // Inflate space on the right
    159             space = new Space(context);
    160             mRow.addView(space);
    161             space.setLayoutParams(new LinearLayout.LayoutParams(0,
    162                     ViewGroup.LayoutParams.MATCH_PARENT, SPACERS_WEIGHT));
    163         }
    164 
    165         mDefaultExpandCollapseView = createIconButton(context, R.drawable.ic_overflow);
    166         mDefaultExpandCollapseView.setContentDescription(context.getString(
    167                 R.string.action_bar_expand_collapse_button));
    168         mDefaultExpandCollapseView.setOnClickListener(v -> onExpandCollapse());
    169     }
    170 
    171     /**
    172      * Returns an index in the {@link #mSlots} array, given a well-known slot position.
    173      */
    174     private int getSlotIndex(@SlotPosition int slotPosition) {
    175         switch (slotPosition) {
    176             case SLOT_MAIN:
    177                 return mNumColumns / 2;
    178             case SLOT_LEFT:
    179                 return mNumColumns < 3 ? -1 : (mNumColumns / 2) - 1;
    180             case SLOT_RIGHT:
    181                 return mNumColumns < 2 ? -1 : (mNumColumns / 2) + 1;
    182             case SLOT_EXPAND_COLLAPSE:
    183                 return mNumColumns - 1;
    184             default:
    185                 throw new IllegalArgumentException("Unknown position: " + slotPosition);
    186         }
    187     }
    188 
    189     /**
    190      * Sets or clears the view to be displayed at a particular position.
    191      *
    192      * @param view view to be displayed, or null to leave the position available.
    193      * @param slotPosition position to update
    194      */
    195     public void setView(@Nullable View view, @SlotPosition int slotPosition) {
    196         if (view != null) {
    197             mFixedViews.put(slotPosition, view);
    198         } else {
    199             mFixedViews.remove(slotPosition);
    200         }
    201         updateViewsLayout();
    202     }
    203 
    204     /**
    205      * Sets the view to use for the expand/collapse action. If not provided, a default
    206      * {@link ImageButton} will be used. The provided {@link View} should be able be able to display
    207      * changes in the "activated" state appropriately.
    208      *
    209      * @param view {@link View} to use for the expand/collapse action.
    210      */
    211     public void setExpandCollapseView(@NonNull View view) {
    212         mExpandCollapseView = view;
    213         mExpandCollapseView.setOnClickListener(v -> onExpandCollapse());
    214         updateViewsLayout();
    215     }
    216 
    217     private View getExpandCollapseView() {
    218         return mExpandCollapseView != null ? mExpandCollapseView : mDefaultExpandCollapseView;
    219     }
    220 
    221     private ImageButton createIconButton(Context context, @DrawableRes int iconResId) {
    222         ImageButton button = (ImageButton) inflate(context, R.layout.action_bar_button, null);
    223         Drawable icon = context.getDrawable(iconResId);
    224         button.setImageDrawable(icon);
    225         return button;
    226     }
    227 
    228     /**
    229      * Sets the views to include in each available slot of the action bar. Slots will be filled from
    230      * start to end (i.e: left to right) and from bottom to top. If more views than available slots
    231      * are provided, all extra views will be ignored.
    232      *
    233      * @param views array of views to include in each available slot.
    234      */
    235     public void setViews(@Nullable View[] views) {
    236         mViews = views;
    237         updateViewsLayout();
    238     }
    239 
    240     private void updateViewsLayout() {
    241         // Prepare an array of positions taken
    242         int totalSlots = mSlots.length;
    243         View[] slotViews = new View[totalSlots];
    244 
    245         // Take all known positions
    246         for (int i = 0; i < mFixedViews.size(); i++) {
    247             int index = getSlotIndex(mFixedViews.keyAt(i));
    248             if (index >= 0 && index < slotViews.length) {
    249                 slotViews[index] = mFixedViews.valueAt(i);
    250             }
    251         }
    252 
    253         // Set all views using both the fixed and flexible positions
    254         int expandCollapseIndex = getSlotIndex(SLOT_EXPAND_COLLAPSE);
    255         int lastUsedIndex = 0;
    256         int viewsIndex = 0;
    257         for (int i = 0; i < totalSlots; i++) {
    258             View viewToUse = null;
    259 
    260             if (slotViews[i] != null) {
    261                 // If there is a view assigned for this slot, use it.
    262                 viewToUse = slotViews[i];
    263             } else if (i == expandCollapseIndex && mViews != null
    264                     && viewsIndex < mViews.length - 1) {
    265                 // If this is the expand/collapse slot, use the corresponding view
    266                 viewToUse = getExpandCollapseView();
    267             } else if (mViews != null && viewsIndex < mViews.length) {
    268                 // Otherwise, if the slot is not reserved, and if we still have views to assign,
    269                 // take one and assign it to this slot.
    270                 viewToUse = mViews[viewsIndex];
    271                 viewsIndex++;
    272             }
    273             setView(viewToUse, mSlots[i]);
    274             if (viewToUse != null) {
    275                 lastUsedIndex = i;
    276             }
    277         }
    278 
    279         mNumExtraRowsInUse = lastUsedIndex / mNumColumns;
    280     }
    281 
    282     private void setView(@Nullable View view, FrameLayout container) {
    283         container.removeAllViews();
    284         if (view != null) {
    285             container.addView(view);
    286             container.setVisibility(VISIBLE);
    287             view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    288                     ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
    289         } else {
    290             container.setVisibility(INVISIBLE);
    291         }
    292     }
    293 
    294     private void onExpandCollapse() {
    295         mIsExpanded = !mIsExpanded;
    296         mSlots[getSlotIndex(SLOT_EXPAND_COLLAPSE)].setActivated(mIsExpanded);
    297 
    298         int animationDuration = getContext().getResources().getInteger(mIsExpanded
    299                 ? R.integer.car_action_bar_expand_anim_duration
    300                 : R.integer.car_action_bar_collapse_anim_duration);
    301         TransitionSet set = new TransitionSet()
    302                 .addTransition(new ChangeBounds())
    303                 .addTransition(new Fade())
    304                 .setDuration(animationDuration)
    305                 .setInterpolator(new FastOutSlowInInterpolator());
    306         TransitionManager.beginDelayedTransition(mActionBarWrapper, set);
    307         for (int i = 0; i < mNumExtraRowsInUse; i++) {
    308             mRowsContainer.getChildAt(i).setVisibility(mIsExpanded ? View.VISIBLE : View.GONE);
    309         }
    310     }
    311 
    312     /**
    313      * Returns the view assigned to the given row and column, after layout.
    314      *
    315      * @param rowIdx row index from 0 being the top row, and {@link #mNumRows{ -1 being the bottom
    316      * row.
    317      * @param colIdx column index from 0 on start (left), to {@link #mNumColumns} on end (right)
    318      */
    319     @VisibleForTesting
    320     @Nullable
    321     View getViewAt(int rowIdx, int colIdx) {
    322         if (rowIdx < 0 || rowIdx > mRowsContainer.getChildCount()) {
    323             throw new IllegalArgumentException(String.format((Locale) null,
    324                     "Row index out of range (requested: %d, max: %d)",
    325                     rowIdx, mRowsContainer.getChildCount()));
    326         }
    327         if (colIdx < 0 || colIdx > mNumColumns) {
    328             throw new IllegalArgumentException(String.format((Locale) null,
    329                     "Column index out of range (requested: %d, max: %d)",
    330                     colIdx, mNumColumns));
    331         }
    332         FrameLayout slot = (FrameLayout) ((LinearLayout) mRowsContainer.getChildAt(rowIdx))
    333                 .getChildAt(colIdx + 1);
    334         return slot.getChildCount() > 0 ? slot.getChildAt(0) : null;
    335     }
    336 }
    337