1 package com.android.car.media.widgets; 2 3 import android.annotation.Nullable; 4 import android.content.Context; 5 import android.content.res.TypedArray; 6 import android.graphics.Bitmap; 7 import android.graphics.drawable.Drawable; 8 import android.support.design.widget.TabLayout; 9 import android.transition.Fade; 10 import android.transition.Transition; 11 import android.transition.TransitionManager; 12 import android.util.AttributeSet; 13 import android.util.Log; 14 import android.util.TypedValue; 15 import android.view.LayoutInflater; 16 import android.view.View; 17 import android.view.ViewGroup; 18 import android.widget.ImageView; 19 import android.widget.LinearLayout; 20 import android.widget.RelativeLayout; 21 import android.widget.TextView; 22 23 import com.android.car.media.R; 24 import com.android.car.media.common.MediaItemMetadata; 25 26 import java.util.List; 27 import java.util.Objects; 28 29 /** 30 * Media template application bar. A detailed explanation of all possible states of this 31 * application bar can be seen at {@link AppBarView.State}. 32 */ 33 public class AppBarView extends RelativeLayout { 34 private static final String TAG = "AppBarView"; 35 /** Default number of tabs to show on this app bar */ 36 private static int DEFAULT_MAX_TABS = 4; 37 38 private LinearLayout mTabsContainer; 39 private ImageView mAppIcon; 40 private ImageView mAppSwitchIcon; 41 private ImageView mNavIcon; 42 private ViewGroup mNavIconContainer; 43 private TextView mTitle; 44 private ViewGroup mAppSwitchContainer; 45 private Context mContext; 46 private int mMaxTabs; 47 private Drawable mArrowDropDown; 48 private Drawable mArrowDropUp; 49 private Drawable mArrowBack; 50 private Drawable mCollapse; 51 private State mState = State.BROWSING; 52 private AppBarListener mListener; 53 private int mFadeDuration; 54 private float mSelectedTabAlpha; 55 private float mUnselectedTabAlpha; 56 private MediaItemMetadata mSelectedItem; 57 private String mMediaAppTitle; 58 private Drawable mDefaultIcon; 59 private boolean mContentForwardEnabled; 60 61 /** 62 * Application bar listener 63 */ 64 public interface AppBarListener { 65 /** 66 * Invoked when the user selects an item from the tabs 67 */ 68 void onTabSelected(MediaItemMetadata item); 69 70 /** 71 * Invoked when the user clicks on the back button 72 */ 73 void onBack(); 74 75 /** 76 * Invoked when the user clicks on the collapse button 77 */ 78 void onCollapse(); 79 80 /** 81 * Invoked when the user clicks on the app selection switch 82 */ 83 void onAppSelection(); 84 } 85 86 /** 87 * Possible states of this application bar 88 */ 89 public enum State { 90 /** 91 * Normal application state. If we are able to obtain media items from the media 92 * source application, we display them as tabs. Otherwise we show the application name. 93 */ 94 BROWSING, 95 /** 96 * Indicates that the user has navigated into an element. In this case we show 97 * the name of the element and we disable the back button. 98 */ 99 STACKED, 100 /** 101 * Indicates that we have expanded a view that can be collapsed. We show the 102 * title of the application and a collapse icon 103 */ 104 PLAYING, 105 /** 106 * Used to indicate that the user is inside the app selector. In this case we disable 107 * navigation, we show the title of the application and we show the app switch icon 108 * point up 109 */ 110 APP_SELECTION 111 } 112 113 public AppBarView(Context context) { 114 this(context, null); 115 } 116 117 public AppBarView(Context context, AttributeSet attrs) { 118 this(context, attrs, 0); 119 } 120 121 public AppBarView(Context context, AttributeSet attrs, int defStyleAttr) { 122 this(context, attrs, defStyleAttr, 0); 123 } 124 125 public AppBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 126 super(context, attrs, defStyleAttr, defStyleRes); 127 init(context, attrs, defStyleAttr, defStyleRes); 128 } 129 130 private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 131 TypedArray ta = context.obtainStyledAttributes( 132 attrs, R.styleable.AppBarView, defStyleAttr, defStyleRes); 133 mMaxTabs = ta.getInteger(R.styleable.AppBarView_max_tabs, DEFAULT_MAX_TABS); 134 ta.recycle(); 135 136 LayoutInflater inflater = (LayoutInflater) context 137 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 138 inflater.inflate(R.layout.appbar_view, this, true); 139 140 mContext = context; 141 mTabsContainer = findViewById(R.id.tabs); 142 mNavIcon = findViewById(R.id.nav_icon); 143 mNavIconContainer = findViewById(R.id.nav_icon_container); 144 mNavIconContainer.setOnClickListener(view -> onNavIconClicked()); 145 mAppIcon = findViewById(R.id.app_icon); 146 mAppSwitchIcon = findViewById(R.id.app_switch_icon); 147 mAppSwitchContainer = findViewById(R.id.app_switch_container); 148 mAppSwitchContainer.setOnClickListener(view -> onAppSwitchClicked()); 149 mTitle = findViewById(R.id.title); 150 mArrowDropDown = getResources().getDrawable(R.drawable.ic_arrow_drop_down, null); 151 mArrowDropUp = getResources().getDrawable(R.drawable.ic_arrow_drop_up, null); 152 mArrowBack = getResources().getDrawable(R.drawable.ic_arrow_back, null); 153 mCollapse = getResources().getDrawable(R.drawable.ic_expand_more, null); 154 mFadeDuration = getResources().getInteger(R.integer.app_selector_fade_duration); 155 TypedValue outValue = new TypedValue(); 156 getResources().getValue(R.dimen.browse_tab_alpha_selected, outValue, true); 157 mSelectedTabAlpha = outValue.getFloat(); 158 getResources().getValue(R.dimen.browse_tab_alpha_unselected, outValue, true); 159 mUnselectedTabAlpha = outValue.getFloat(); 160 mMediaAppTitle = getResources().getString(R.string.media_app_title); 161 mDefaultIcon = getResources().getDrawable(R.drawable.ic_music); 162 163 setState(State.BROWSING); 164 } 165 166 private void onNavIconClicked() { 167 if (mListener == null) { 168 return; 169 } 170 switch (mState) { 171 case STACKED: 172 mListener.onBack(); 173 break; 174 case PLAYING: 175 mListener.onCollapse(); 176 break; 177 } 178 } 179 180 private void onAppSwitchClicked() { 181 if (mListener == null) { 182 return; 183 } 184 mListener.onAppSelection(); 185 } 186 187 /** 188 * Sets a listener of this application bar events. In order to avoid memory leaks, consumers 189 * must reset this reference by setting the listener to null. 190 */ 191 public void setListener(AppBarListener listener) { 192 mListener = listener; 193 } 194 195 /** 196 * Updates the list of items to show in the application bar tabs. 197 * 198 * @param items list of tabs to show, or null if no tabs should be shown. 199 */ 200 public void setItems(@Nullable List<MediaItemMetadata> items) { 201 mTabsContainer.removeAllViews(); 202 203 if (items != null) { 204 int count = 0; 205 int padding = mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_4); 206 int tabWidth = mContext.getResources().getDimensionPixelSize(R.dimen.browse_tab_width) + 207 2 * padding; 208 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 209 tabWidth, ViewGroup.LayoutParams.MATCH_PARENT); 210 for (MediaItemMetadata item : items) { 211 MediaItemTabView tab = new MediaItemTabView(mContext, item); 212 mTabsContainer.addView(tab); 213 tab.setLayoutParams(layoutParams); 214 tab.setOnClickListener(view -> { 215 if (mListener != null) { 216 mListener.onTabSelected(item); 217 } 218 }); 219 tab.setPadding(padding, 0, padding, 0); 220 tab.requestLayout(); 221 tab.setTag(item); 222 223 count++; 224 if (count >= mMaxTabs) { 225 break; 226 } 227 } 228 } 229 230 // Refresh the views visibility 231 setState(mState); 232 } 233 234 /** 235 * Updates the title to display when the bar is not showing tabs. 236 */ 237 public void setTitle(CharSequence title) { 238 mTitle.setText(title != null ? title : mMediaAppTitle); 239 } 240 241 /** 242 * Whether content forward browsing is enabled or not 243 */ 244 public void setContentForwardEnabled(boolean enabled) { 245 mContentForwardEnabled = enabled; 246 } 247 248 /** 249 * Updates the application icon to show next to the application switcher. 250 */ 251 public void setAppIcon(Bitmap icon) { 252 if (icon != null) { 253 mAppIcon.setImageBitmap(icon); 254 } else { 255 mAppIcon.setImageDrawable(mDefaultIcon); 256 } 257 } 258 259 /** 260 * Indicates whether or not the application switcher should be enabled. 261 */ 262 public void setAppSelection(boolean enabled) { 263 mAppSwitchIcon.setVisibility(enabled ? View.VISIBLE : View.GONE); 264 } 265 266 /** 267 * Updates the currently active item 268 */ 269 public void setActiveItem(MediaItemMetadata item) { 270 mSelectedItem = item; 271 // TODO(b/79264184): Updating tabs alpha is causing them to disappear randomly. We are 272 // de-activating this feature for not. 273 // updateTabs(); 274 } 275 276 private void updateTabs() { 277 for (int i = 0; i < mTabsContainer.getChildCount(); i++) { 278 View child = mTabsContainer.getChildAt(i); 279 if (child instanceof MediaItemTabView) { 280 MediaItemTabView tabView = (MediaItemTabView) child; 281 boolean match = mSelectedItem != null && Objects.equals( 282 mSelectedItem.getId(), 283 ((MediaItemMetadata) tabView.getTag()).getId()); 284 tabView.setAlpha(match ? mSelectedTabAlpha : mUnselectedTabAlpha); 285 } 286 } 287 } 288 289 /** 290 * Updates the state of the bar. 291 */ 292 public void setState(State state) { 293 boolean hasItems = mTabsContainer.getChildCount() > 0; 294 mState = state; 295 296 Transition transition = new Fade().setDuration(mFadeDuration); 297 TransitionManager.beginDelayedTransition(this, transition); 298 Log.d(TAG, "Updating state: " + state + " (has items: " + hasItems + ")"); 299 switch (state) { 300 case BROWSING: 301 mNavIconContainer.setVisibility(View.GONE); 302 mTabsContainer.setVisibility(hasItems ? View.VISIBLE : View.GONE); 303 mTitle.setVisibility(hasItems ? View.GONE : View.VISIBLE); 304 mAppSwitchIcon.setImageDrawable(mArrowDropDown); 305 break; 306 case STACKED: 307 mNavIcon.setImageDrawable(mArrowBack); 308 mNavIconContainer.setVisibility(View.VISIBLE); 309 mTabsContainer.setVisibility(View.GONE); 310 mTitle.setVisibility(View.VISIBLE); 311 mAppSwitchIcon.setImageDrawable(mArrowDropDown); 312 break; 313 case PLAYING: 314 mNavIcon.setImageDrawable(mCollapse); 315 mNavIconContainer.setVisibility(hasItems || !mContentForwardEnabled ? View.GONE 316 : View.VISIBLE); 317 mTabsContainer.setVisibility(hasItems && mContentForwardEnabled ? View.VISIBLE 318 : View.GONE); 319 mTitle.setVisibility(hasItems || !mContentForwardEnabled ? View.GONE 320 : View.VISIBLE); 321 mAppSwitchIcon.setImageDrawable(mArrowDropDown); 322 break; 323 case APP_SELECTION: 324 mNavIconContainer.setVisibility(View.GONE); 325 mTabsContainer.setVisibility(View.GONE); 326 mTitle.setVisibility(mContentForwardEnabled ? View.VISIBLE : View.GONE); 327 mAppSwitchIcon.setImageDrawable(mArrowDropUp); 328 break; 329 } 330 } 331 } 332