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