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 com.android.car.apps.common; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.drawable.Drawable; 22 import android.transition.ChangeBounds; 23 import android.transition.Fade; 24 import android.transition.TransitionManager; 25 import android.transition.TransitionSet; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.util.SparseArray; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.FrameLayout; 33 import android.widget.ImageButton; 34 import android.widget.LinearLayout; 35 import android.widget.RelativeLayout; 36 import android.widget.Space; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 import androidx.annotation.VisibleForTesting; 41 import androidx.core.util.Preconditions; 42 import androidx.interpolator.view.animation.FastOutSlowInInterpolator; 43 44 import java.util.Locale; 45 46 47 /** 48 * An actions panel with three distinctive zones: 49 * <ul> 50 * <li>Main control: located in the bottom center it shows a highlighted icon and a circular 51 * progress bar. 52 * <li>Secondary controls: these are displayed at the left and at the right of the main control. 53 * <li>Overflow controls: these are displayed at the left and at the right of the secondary controls 54 * (if the space allows) and on the additional space if the panel is expanded. 55 * </ul> 56 */ 57 public class ControlBar extends RelativeLayout implements ExpandableControlBar { 58 private static final String TAG = "ControlBar"; 59 60 // Rows container 61 private ViewGroup mRowsContainer; 62 // All slots in this action bar where 0 is the bottom-start corner of the matrix, and 63 // mNumColumns * nNumRows - 1 is the top-end corner 64 private FrameLayout[] mSlots; 65 /** 66 * Reference to the first slot we create. Used to properly inflate buttons without loosing 67 * their layout params. 68 */ 69 private FrameLayout mFirstCreatedSlot; 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 (at least one!). 86 private int mNumRows; 87 // Whether the expand button should be visible or not 88 private boolean mExpandEnabled; 89 // Callback for the expand/collapse button 90 private ExpandCollapseCallback mExpandCollapseCallback; 91 92 // Default number of columns, if unspecified 93 private static final int DEFAULT_COLUMNS = 3; 94 // Weight for the spacers used between buttons 95 private static final float SPACERS_WEIGHT = 1f; 96 ControlBar(Context context)97 public ControlBar(Context context) { 98 super(context); 99 init(context, null, 0, 0); 100 } 101 ControlBar(Context context, AttributeSet attrs)102 public ControlBar(Context context, AttributeSet attrs) { 103 super(context, attrs); 104 init(context, attrs, 0, 0); 105 } 106 ControlBar(Context context, AttributeSet attrs, int defStyleAttrs)107 public ControlBar(Context context, AttributeSet attrs, int defStyleAttrs) { 108 super(context, attrs, defStyleAttrs); 109 init(context, attrs, defStyleAttrs, 0); 110 } 111 ControlBar(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes)112 public ControlBar(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { 113 super(context, attrs, defStyleAttrs, defStyleRes); 114 init(context, attrs, defStyleAttrs, defStyleRes); 115 } 116 init(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes)117 private void init(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { 118 inflate(context, R.layout.control_bar, this); 119 120 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ControlBar, 121 defStyleAttrs, defStyleRes); 122 mNumColumns = ta.getInteger(R.styleable.ControlBar_columns, DEFAULT_COLUMNS); 123 mExpandEnabled = ta.getBoolean(R.styleable.ControlBar_enableOverflow, true); 124 ta.recycle(); 125 126 mRowsContainer = findViewById(R.id.rows_container); 127 mNumRows = mRowsContainer.getChildCount(); 128 Preconditions.checkState(mNumRows > 0, "Must have at least 1 row"); 129 130 mSlots = new FrameLayout[mNumColumns * mNumRows]; 131 132 LayoutInflater inflater = LayoutInflater.from(context); 133 final boolean attachToRoot = false; 134 135 for (int i = 0; i < mNumRows; i++) { 136 // Slots are reserved in reverse order (first slots are in the bottom row) 137 ViewGroup row = (ViewGroup) mRowsContainer.getChildAt(mNumRows - i - 1); 138 // Inflate necessary number of columns 139 for (int j = 0; j < mNumColumns; j++) { 140 int pos = i * mNumColumns + j; 141 mSlots[pos] = (FrameLayout) inflater.inflate(R.layout.control_bar_slot, row, 142 attachToRoot); 143 if (mFirstCreatedSlot == null) { 144 mFirstCreatedSlot = mSlots[pos]; 145 } 146 if (j > 0) { 147 Space space = new Space(context); 148 row.addView(space); 149 space.setLayoutParams(new LinearLayout.LayoutParams(0, 150 ViewGroup.LayoutParams.MATCH_PARENT, SPACERS_WEIGHT)); 151 } 152 row.addView(mSlots[pos]); 153 } 154 } 155 156 mDefaultExpandCollapseView = createIconButton( 157 context.getDrawable(R.drawable.ic_overflow_button)); 158 mDefaultExpandCollapseView.setContentDescription(context.getString( 159 R.string.control_bar_expand_collapse_button)); 160 mDefaultExpandCollapseView.setOnClickListener(v -> onExpandCollapse()); 161 } 162 getSlotIndex(@lotPosition int slotPosition)163 private int getSlotIndex(@SlotPosition int slotPosition) { 164 return CarControlBar.getSlotIndex(slotPosition, mNumColumns); 165 } 166 167 @Override setView(@ullable View view, @SlotPosition int slotPosition)168 public void setView(@Nullable View view, @SlotPosition int slotPosition) { 169 if (view != null) { 170 mFixedViews.put(slotPosition, view); 171 } else { 172 mFixedViews.remove(slotPosition); 173 } 174 updateViewsLayout(); 175 } 176 177 /** 178 * Sets the view to use for the expand/collapse action. If not provided, a default 179 * {@link ImageButton} will be used. The provided {@link View} should be able be able to display 180 * changes in the "activated" state appropriately. 181 * 182 * @param view {@link View} to use for the expand/collapse action. 183 */ setExpandCollapseView(@onNull View view)184 public void setExpandCollapseView(@NonNull View view) { 185 mExpandCollapseView = view; 186 mExpandCollapseView.setOnClickListener(v -> onExpandCollapse()); 187 updateViewsLayout(); 188 } 189 getExpandCollapseView()190 private View getExpandCollapseView() { 191 return mExpandCollapseView != null ? mExpandCollapseView : mDefaultExpandCollapseView; 192 } 193 194 @Override createIconButton(Drawable icon)195 public ImageButton createIconButton(Drawable icon) { 196 return createIconButton(icon, R.layout.control_bar_button); 197 } 198 199 @Override createIconButton(Drawable icon, int viewId)200 public ImageButton createIconButton(Drawable icon, int viewId) { 201 LayoutInflater inflater = LayoutInflater.from(mFirstCreatedSlot.getContext()); 202 final boolean attachToRoot = false; 203 ImageButton button = (ImageButton) inflater.inflate(viewId, mFirstCreatedSlot, 204 attachToRoot); 205 button.setImageDrawable(icon); 206 return button; 207 } 208 209 @Override registerExpandCollapseCallback(@ullable ExpandCollapseCallback callback)210 public void registerExpandCollapseCallback(@Nullable ExpandCollapseCallback callback) { 211 mExpandCollapseCallback = callback; 212 } 213 214 @Override close()215 public void close() { 216 if (mIsExpanded) { 217 onExpandCollapse(); 218 } 219 } 220 221 @Override setViews(@ullable View[] views)222 public void setViews(@Nullable View[] views) { 223 mViews = views; 224 updateViewsLayout(); 225 } 226 updateViewsLayout()227 private void updateViewsLayout() { 228 // Prepare an array of positions taken 229 int totalSlots = mSlots.length; 230 View[] slotViews = new View[totalSlots]; 231 232 // Take all known positions 233 for (int i = 0; i < mFixedViews.size(); i++) { 234 int index = getSlotIndex(mFixedViews.keyAt(i)); 235 if (index >= 0 && index < slotViews.length) { 236 slotViews[index] = mFixedViews.valueAt(i); 237 } 238 } 239 240 // Set all views using both the fixed and flexible positions 241 int expandCollapseIndex = getSlotIndex(SLOT_EXPAND_COLLAPSE); 242 int lastUsedIndex = 0; 243 int viewsIndex = 0; 244 for (int i = 0; i < totalSlots; i++) { 245 View viewToUse = null; 246 247 if (slotViews[i] != null) { 248 // If there is a view assigned for this slot, use it. 249 viewToUse = slotViews[i]; 250 } else if (mExpandEnabled && i == expandCollapseIndex && mViews != null 251 && viewsIndex < mViews.length - 1) { 252 // If this is the expand/collapse slot, use the corresponding view 253 viewToUse = getExpandCollapseView(); 254 Log.d(TAG, "" + this + "Setting expand control"); 255 } else if (mViews != null && viewsIndex < mViews.length) { 256 // Otherwise, if the slot is not reserved, and if we still have views to assign, 257 // take one and assign it to this slot. 258 viewToUse = mViews[viewsIndex]; 259 viewsIndex++; 260 } 261 setView(viewToUse, mSlots[i]); 262 if (viewToUse != null) { 263 lastUsedIndex = i; 264 } 265 } 266 267 mNumExtraRowsInUse = lastUsedIndex / mNumColumns; 268 final int lastIndex = lastUsedIndex; 269 270 if (mNumRows > 1) { 271 // Align expanded control bar rows 272 mRowsContainer.getViewTreeObserver().addOnGlobalLayoutListener(() -> { 273 for (int i = 1; i < mNumRows; i++) { 274 // mRowsContainer's children are in reverse order (last row is at index 0) 275 int rowIndex = mNumRows - 1 - i; 276 if (lastIndex < (i + 1) * mNumColumns) { 277 // Align the last row's center with the first row by translating the last 278 // row by half the difference between the two rows' length. 279 // We use the position of the last slot as a proxy for the length, since the 280 // slots have the same size, and both rows have the same start point. 281 float lastRowX = mSlots[lastIndex].getX(); 282 float firstRowX = mSlots[mNumColumns - 1].getX(); 283 mRowsContainer.getChildAt(rowIndex).setTranslationX( 284 (firstRowX - lastRowX) / 2); 285 } else { 286 mRowsContainer.getChildAt(rowIndex).setTranslationX(0); 287 } 288 } 289 }); 290 } 291 } 292 setView(@ullable View view, FrameLayout container)293 private void setView(@Nullable View view, FrameLayout container) { 294 if (view != null) { 295 // Don't set the view if it stays the same. 296 if (container.getChildCount() == 1 && container.getChildAt(0) == view) { 297 return; 298 } 299 300 ViewGroup parent = (ViewGroup) view.getParent(); 301 // As we are removing views (on BT disconnect, for example), some items will be 302 // shifting from expanded to collapsed (like Queue item) - remove those from the 303 // group before adding to the new slot 304 if (view.getParent() != null) { 305 parent.removeView(view); 306 } 307 container.removeAllViews(); 308 container.addView(view); 309 container.setVisibility(VISIBLE); 310 } else { 311 if (container.getChildCount() != 0) { 312 container.removeAllViews(); 313 } 314 container.setVisibility(INVISIBLE); 315 } 316 } 317 onExpandCollapse()318 private void onExpandCollapse() { 319 mIsExpanded = !mIsExpanded; 320 if (mExpandCollapseView != null) { 321 mExpandCollapseView.setSelected(mIsExpanded); 322 } 323 if (mExpandCollapseCallback != null) { 324 mExpandCollapseCallback.onExpandCollapse(mIsExpanded); 325 } 326 mSlots[getSlotIndex(SLOT_EXPAND_COLLAPSE)].setActivated(mIsExpanded); 327 328 int animationDuration = getContext().getResources().getInteger(mIsExpanded 329 ? R.integer.control_bar_expand_anim_duration 330 : R.integer.control_bar_collapse_anim_duration); 331 TransitionSet set = new TransitionSet() 332 .addTransition(new ChangeBounds()) 333 .addTransition(new Fade()) 334 .setDuration(animationDuration) 335 .setInterpolator(new FastOutSlowInInterpolator()); 336 TransitionManager.beginDelayedTransition(this, set); 337 for (int i = 0; i < mNumExtraRowsInUse; i++) { 338 mRowsContainer.getChildAt(i).setVisibility(mIsExpanded ? View.VISIBLE : View.GONE); 339 } 340 } 341 342 /** 343 * Returns the view assigned to the given row and column, after layout. 344 * 345 * @param rowIdx row index from 0 being the top row, and {@link #mNumRows{ -1 being the bottom 346 * row. 347 * @param colIdx column index from 0 on start (left), to {@link #mNumColumns} on end (right) 348 */ 349 @VisibleForTesting 350 @Nullable getViewAt(int rowIdx, int colIdx)351 View getViewAt(int rowIdx, int colIdx) { 352 if (rowIdx < 0 || rowIdx > mRowsContainer.getChildCount()) { 353 throw new IllegalArgumentException(String.format((Locale) null, 354 "Row index out of range (requested: %d, max: %d)", 355 rowIdx, mRowsContainer.getChildCount())); 356 } 357 if (colIdx < 0 || colIdx > mNumColumns) { 358 throw new IllegalArgumentException(String.format((Locale) null, 359 "Column index out of range (requested: %d, max: %d)", 360 colIdx, mNumColumns)); 361 } 362 FrameLayout slot = (FrameLayout) ((LinearLayout) mRowsContainer.getChildAt(rowIdx)) 363 .getChildAt(colIdx + 1); 364 return slot.getChildCount() > 0 ? slot.getChildAt(0) : null; 365 } 366 } 367