1 /* 2 * Copyright (C) 2019 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.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.Typeface; 22 import android.graphics.drawable.Drawable; 23 import android.util.ArraySet; 24 import android.util.AttributeSet; 25 import android.view.Gravity; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.BaseAdapter; 30 import android.widget.ImageView; 31 import android.widget.LinearLayout; 32 import android.widget.TextView; 33 34 import androidx.annotation.LayoutRes; 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 38 import com.android.car.apps.common.R; 39 import com.android.car.apps.common.util.Themes; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 import java.util.Set; 44 45 /** 46 * Custom tab layout which supports adding tabs dynamically 47 * 48 * <p>It supports two layout modes: 49 * <ul><li>Flexible layout which will fill the width 50 * <li>Non-flexible layout which wraps content with a minimum tab width. By setting tab gravity, 51 * it can left aligned, right aligned or center aligned. 52 * 53 * <p>Scrolling function is not supported. If a tab item runs out of the tab layout bound, there 54 * is no way to access it. It's better to set the layout mode to flexible in this case. 55 * 56 * <p>Default tab item inflates from R.layout.car_tab_item, but it also supports custom layout id. 57 * By doing this, appearance of tab item view can be customized. 58 * 59 * <p>Touch feedback is using @android:attr/selectableItemBackground. 60 * 61 * @param <T> Presents a CarTab entity 62 */ 63 public class CarTabLayout<T extends CarTabLayout.CarTab> extends LinearLayout { 64 65 /** 66 * Listener that listens the car tab selection change. 67 * 68 * @param <T> Presents a CarTab entity that has state update on a tab select action 69 */ 70 public interface OnCarTabSelectedListener<T extends CarTab> { 71 /** Callback triggered when a car tab is selected. */ onCarTabSelected(T carTab)72 void onCarTabSelected(T carTab); 73 74 /** Callback triggered when a car tab is unselected. */ onCarTabUnselected(T carTab)75 void onCarTabUnselected(T carTab); 76 77 /** Callback triggered when a car tab is reselected. */ onCarTabReselected(T carTab)78 void onCarTabReselected(T carTab); 79 } 80 81 /** 82 * No-op implementation of {@link OnCarTabSelectedListener}. 83 * 84 * @param <T> See {@link OnCarTabSelectedListener} 85 */ 86 public static class SimpleOnCarTabSelectedListener<T extends CarTab> implements 87 OnCarTabSelectedListener<T> { 88 89 @Override onCarTabSelected(T carTab)90 public void onCarTabSelected(T carTab) { 91 // No-op 92 } 93 94 @Override onCarTabUnselected(T carTab)95 public void onCarTabUnselected(T carTab) { 96 // No-op 97 } 98 99 @Override onCarTabReselected(T carTab)100 public void onCarTabReselected(T carTab) { 101 // No-op 102 } 103 } 104 105 // View attributes 106 private final boolean mTabFlexibleLayout; 107 private final int mTabPaddingX; 108 109 private final Set<OnCarTabSelectedListener<T>> mOnCarTabSelectedListeners; 110 111 private final CarTabAdapter<T> mCarTabAdapter; 112 CarTabLayout(@onNull Context context)113 public CarTabLayout(@NonNull Context context) { 114 this(context, null); 115 } 116 CarTabLayout(@onNull Context context, @Nullable AttributeSet attrs)117 public CarTabLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 118 this(context, attrs, 0); 119 } 120 CarTabLayout(@onNull Context context, @Nullable AttributeSet attrs, int defStyle)121 public CarTabLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { 122 super(context, attrs, defStyle); 123 mOnCarTabSelectedListeners = new ArraySet<>(); 124 125 TypedArray ta = context.obtainStyledAttributes( 126 attrs, R.styleable.CarTabLayout, defStyle, 0); 127 mTabPaddingX = ta.getDimensionPixelSize(R.styleable.CarTabLayout_tabPaddingX, 128 context.getResources().getDimensionPixelSize(R.dimen.car_tab_padding_x)); 129 mTabFlexibleLayout = ta.getBoolean(R.styleable.CarTabLayout_tabFlexibleLayout, 130 context.getResources().getBoolean(R.bool.car_tab_flexible_layout)); 131 int tabItemLayout = ta.getResourceId(R.styleable.CarTabLayout_tabItemLayout, 132 R.layout.car_tab_item); 133 ta.recycle(); 134 135 mCarTabAdapter = new CarTabAdapter(context, tabItemLayout, this); 136 } 137 138 /** 139 * Add a tab to this layout. The tab will be added at the end of the list. If this is the first 140 * tab to be added it will become the selected tab. 141 */ addCarTab(T carTab)142 public void addCarTab(T carTab) { 143 mCarTabAdapter.add(carTab); 144 // If there is only one tab in the group, set it to be selected. 145 if (mCarTabAdapter.getCount() == 1) { 146 mCarTabAdapter.selectCarTab(0); 147 } 148 } 149 150 /** Set the tab as the current selected tab. */ selectCarTab(T carTab)151 public void selectCarTab(T carTab) { 152 mCarTabAdapter.selectCarTab(carTab); 153 } 154 155 /** Set the tab at given position as the current selected tab. */ selectCarTab(int position)156 public void selectCarTab(int position) { 157 mCarTabAdapter.selectCarTab(position); 158 } 159 160 /** Returns how tab items it has. */ getCarTabCount()161 public int getCarTabCount() { 162 return mCarTabAdapter.getCount(); 163 } 164 165 /** Returns the position of the given car tab. */ getCarTabPosition(T carTab)166 public int getCarTabPosition(T carTab) { 167 return mCarTabAdapter.getPosition(carTab); 168 } 169 170 /** Return the car tab at the given position. */ get(int position)171 public T get(int position) { 172 return mCarTabAdapter.getItem(position); 173 } 174 175 /** Clear all car tabs. */ clearAllCarTabs()176 public void clearAllCarTabs() { 177 mCarTabAdapter.clear(); 178 } 179 180 /** Register a {@link OnCarTabSelectedListener}. Same listener will only be registered once. */ addOnCarTabSelectedListener( @onNull OnCarTabSelectedListener onCarTabSelectedListener)181 public void addOnCarTabSelectedListener( 182 @NonNull OnCarTabSelectedListener onCarTabSelectedListener) { 183 mOnCarTabSelectedListeners.add(onCarTabSelectedListener); 184 } 185 186 /** Unregister a {@link OnCarTabSelectedListener} */ removeOnCarTabSelectedListener( @onNull OnCarTabSelectedListener onCarTabSelectedListener)187 public void removeOnCarTabSelectedListener( 188 @NonNull OnCarTabSelectedListener onCarTabSelectedListener) { 189 mOnCarTabSelectedListeners.remove(onCarTabSelectedListener); 190 } 191 dispatchOnCarTabSelected(T carTab)192 private void dispatchOnCarTabSelected(T carTab) { 193 for (OnCarTabSelectedListener onCarTabSelectedListener : mOnCarTabSelectedListeners) { 194 onCarTabSelectedListener.onCarTabSelected(carTab); 195 } 196 } 197 dispatchOnCarTabUnselected(T carTab)198 private void dispatchOnCarTabUnselected(T carTab) { 199 for (OnCarTabSelectedListener onCarTabSelectedListener : mOnCarTabSelectedListeners) { 200 onCarTabSelectedListener.onCarTabUnselected(carTab); 201 } 202 } 203 dispatchOnCarTabReselected(T carTab)204 private void dispatchOnCarTabReselected(T carTab) { 205 for (OnCarTabSelectedListener onCarTabSelectedListener : mOnCarTabSelectedListeners) { 206 onCarTabSelectedListener.onCarTabReselected(carTab); 207 } 208 } 209 addCarTabView(View carTabView, int position)210 private void addCarTabView(View carTabView, int position) { 211 LayoutParams layoutParams; 212 if (mTabFlexibleLayout) { 213 layoutParams = new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT); 214 layoutParams.weight = 1; 215 } else { 216 layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 217 ViewGroup.LayoutParams.MATCH_PARENT); 218 } 219 addView(carTabView, position, layoutParams); 220 } 221 createCarTabItemView()222 private ViewGroup createCarTabItemView() { 223 LinearLayout carTabItemView = new LinearLayout(mContext); 224 carTabItemView.setOrientation(LinearLayout.VERTICAL); 225 carTabItemView.setGravity(Gravity.CENTER); 226 carTabItemView.setPadding(mTabPaddingX, 0, mTabPaddingX, 0); 227 Drawable backgroundDrawable = Themes.getAttrDrawable(getContext(), 228 R.style.CarTabItemBackground, android.R.attr.background); 229 carTabItemView.setBackground(backgroundDrawable); 230 return carTabItemView; 231 } 232 233 private static class CarTabAdapter<T extends CarTab> extends BaseAdapter { 234 private static final int MEDIUM_WEIGHT = 500; 235 private final Context mContext; 236 private final CarTabLayout mCarTabLayout; 237 @LayoutRes 238 private final int mCarTabItemLayoutRes; 239 private final Typeface mUnselectedTypeface; 240 private final Typeface mSelectedTypeface; 241 private final List<T> mCarTabList; 242 CarTabAdapter(Context context, @LayoutRes int res, CarTabLayout carTabLayout)243 private CarTabAdapter(Context context, @LayoutRes int res, CarTabLayout carTabLayout) { 244 mCarTabList = new ArrayList<>(); 245 mContext = context; 246 mCarTabItemLayoutRes = res; 247 mCarTabLayout = carTabLayout; 248 mUnselectedTypeface = Typeface.defaultFromStyle(Typeface.NORMAL); 249 // TODO: add indirection to allow customization. 250 mSelectedTypeface = Typeface.create(mUnselectedTypeface, MEDIUM_WEIGHT, false); 251 } 252 add(@onNull T carTab)253 private void add(@NonNull T carTab) { 254 mCarTabList.add(carTab); 255 notifyItemInserted(mCarTabList.size() - 1); 256 } 257 clear()258 private void clear() { 259 mCarTabList.clear(); 260 mCarTabLayout.removeAllViews(); 261 } 262 getPosition(CarTab carTab)263 private int getPosition(CarTab carTab) { 264 return mCarTabList.indexOf(carTab); 265 } 266 267 @Override getCount()268 public int getCount() { 269 return mCarTabList.size(); 270 } 271 272 @Override getItem(int position)273 public T getItem(int position) { 274 return mCarTabList.get(position); 275 } 276 277 @Override getItemId(int position)278 public long getItemId(int position) { 279 return position; 280 } 281 282 @Override 283 @NonNull getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)284 public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { 285 ViewGroup carTabItemView = mCarTabLayout.createCarTabItemView(); 286 LayoutInflater.from(mContext).inflate(mCarTabItemLayoutRes, carTabItemView, true); 287 288 presentCarTabItemView(position, carTabItemView); 289 return carTabItemView; 290 } 291 selectCarTab(CarTab carTab)292 private void selectCarTab(CarTab carTab) { 293 selectCarTab(getPosition(carTab)); 294 } 295 selectCarTab(int position)296 private void selectCarTab(int position) { 297 if (position < 0 || position >= getCount()) { 298 throw new IndexOutOfBoundsException("Invalid position"); 299 } 300 301 for (int i = 0; i < getCount(); i++) { 302 CarTab carTabItem = mCarTabList.get(i); 303 boolean isTabSelected = position == i; 304 if (carTabItem.mIsSelected != isTabSelected) { 305 carTabItem.mIsSelected = isTabSelected; 306 notifyItemChanged(i); 307 if (carTabItem.mIsSelected) { 308 mCarTabLayout.dispatchOnCarTabSelected(carTabItem); 309 } else { 310 mCarTabLayout.dispatchOnCarTabUnselected(carTabItem); 311 } 312 } else if (carTabItem.mIsSelected) { 313 mCarTabLayout.dispatchOnCarTabReselected(carTabItem); 314 } 315 } 316 } 317 318 /** Represent the car tab item at given position without destroying and recreating UI. */ notifyItemChanged(int position)319 private void notifyItemChanged(int position) { 320 View carTabItemView = mCarTabLayout.getChildAt(position); 321 presentCarTabItemView(position, carTabItemView); 322 } 323 notifyItemInserted(int position)324 private void notifyItemInserted(int position) { 325 View insertedView = getView(position, null, mCarTabLayout); 326 mCarTabLayout.addCarTabView(insertedView, position); 327 } 328 presentCarTabItemView(int position, @NonNull View carTabItemView)329 private void presentCarTabItemView(int position, @NonNull View carTabItemView) { 330 CarTab carTab = mCarTabList.get(position); 331 332 ImageView iconView = carTabItemView.findViewById(R.id.car_tab_item_icon); 333 TextView textView = carTabItemView.findViewById(R.id.car_tab_item_text); 334 335 carTabItemView.setOnClickListener(view -> selectCarTab(carTab)); 336 carTab.bindText(textView); 337 carTab.bindIcon(iconView); 338 339 carTabItemView.setSelected(carTab.mIsSelected); 340 iconView.setSelected(carTab.mIsSelected); 341 textView.setSelected(carTab.mIsSelected); 342 textView.setTypeface(carTab.mIsSelected ? mSelectedTypeface : mUnselectedTypeface); 343 } 344 } 345 346 /** Car tab entity. */ 347 public static class CarTab { 348 private final Drawable mIcon; 349 private final CharSequence mText; 350 private boolean mIsSelected; 351 CarTab(@ullable Drawable icon, @Nullable CharSequence text)352 public CarTab(@Nullable Drawable icon, @Nullable CharSequence text) { 353 mIcon = icon; 354 mText = text; 355 } 356 357 /** Set tab text. */ bindText(TextView textView)358 protected void bindText(TextView textView) { 359 textView.setText(mText); 360 } 361 362 /** Set icon drawable. */ bindIcon(ImageView imageView)363 protected void bindIcon(ImageView imageView) { 364 imageView.setImageDrawable(mIcon); 365 } 366 } 367 } 368