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 package com.android.car.ui.toolbar; 17 18 import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId; 19 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.drawable.Drawable; 23 import android.util.ArraySet; 24 import android.util.AttributeSet; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.widget.BaseAdapter; 29 import android.widget.ImageView; 30 import android.widget.LinearLayout; 31 import android.widget.TextView; 32 33 import androidx.annotation.LayoutRes; 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.car.ui.R; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Set; 42 43 /** 44 * Custom tab layout which supports adding tabs dynamically 45 * 46 * <p>It supports two layout modes: 47 * <ul><li>Flexible layout which will fill the width 48 * <li>Non-flexible layout which wraps content with a minimum tab width. By setting tab gravity, 49 * it can left aligned, right aligned or center aligned. 50 * 51 * <p>Scrolling function is not supported. If a tab item runs out of the tab layout bound, there 52 * is no way to access it. It's better to set the layout mode to flexible in this case. 53 * 54 * <p>Default tab item inflates from R.layout.car_ui_tab_item, but it also supports custom layout 55 * id, by overlaying R.layout.car_ui_tab_item_layout. By doing this, appearance of tab item view 56 * can be customized. 57 * 58 * <p>Touch feedback is using @android:attr/selectableItemBackground. 59 */ 60 public class TabLayout extends LinearLayout { 61 62 /** 63 * Listener that listens the tab selection change. 64 */ 65 public interface Listener { 66 /** Callback triggered when a tab is selected. */ onTabSelected(Tab tab)67 default void onTabSelected(Tab tab) { 68 } 69 70 /** Callback triggered when a tab is unselected. */ onTabUnselected(Tab tab)71 default void onTabUnselected(Tab tab) { 72 } 73 74 /** Callback triggered when a tab is reselected. */ onTabReselected(Tab tab)75 default void onTabReselected(Tab tab) { 76 } 77 } 78 79 private final Set<Listener> mListeners = new ArraySet<>(); 80 81 private final TabAdapter mTabAdapter; 82 TabLayout(@onNull Context context)83 public TabLayout(@NonNull Context context) { 84 this(context, null); 85 } 86 TabLayout(@onNull Context context, @Nullable AttributeSet attrs)87 public TabLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 88 this(context, attrs, 0); 89 } 90 TabLayout(@onNull Context context, @Nullable AttributeSet attrs, int defStyle)91 public TabLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { 92 super(context, attrs, defStyle); 93 Resources resources = context.getResources(); 94 95 boolean tabFlexibleLayout = resources.getBoolean(R.bool.car_ui_toolbar_tab_flexible_layout); 96 @LayoutRes int tabLayoutRes = tabFlexibleLayout 97 ? R.layout.car_ui_toolbar_tab_item_layout_flexible 98 : R.layout.car_ui_toolbar_tab_item_layout; 99 mTabAdapter = new TabAdapter(context, tabLayoutRes, this); 100 } 101 102 /** 103 * Add a tab to this layout. The tab will be added at the end of the list. If this is the first 104 * tab to be added it will become the selected tab. 105 */ addTab(Tab tab)106 public void addTab(Tab tab) { 107 mTabAdapter.add(tab); 108 // If there is only one tab in the group, set it to be selected. 109 if (mTabAdapter.getCount() == 1) { 110 mTabAdapter.selectTab(0); 111 } 112 } 113 114 /** Set the tab as the current selected tab. */ selectTab(Tab tab)115 public void selectTab(Tab tab) { 116 mTabAdapter.selectTab(tab); 117 } 118 119 /** Set the tab at given position as the current selected tab. */ selectTab(int position)120 public void selectTab(int position) { 121 mTabAdapter.selectTab(position); 122 } 123 124 /** Returns how tab items it has. */ getTabCount()125 public int getTabCount() { 126 return mTabAdapter.getCount(); 127 } 128 129 /** Returns the position of the given tab. */ getTabPosition(Tab tab)130 public int getTabPosition(Tab tab) { 131 return mTabAdapter.getPosition(tab); 132 } 133 134 /** Return the tab at the given position. */ get(int position)135 public Tab get(int position) { 136 return mTabAdapter.getItem(position); 137 } 138 139 /** Clear all tabs. */ clearAllTabs()140 public void clearAllTabs() { 141 mTabAdapter.clear(); 142 } 143 144 /** Register a {@link Listener}. Same listener will only be registered once. */ addListener(@onNull Listener listener)145 public void addListener(@NonNull Listener listener) { 146 mListeners.add(listener); 147 } 148 149 /** Unregister a {@link Listener} */ removeListener(@onNull Listener listener)150 public void removeListener(@NonNull Listener listener) { 151 mListeners.remove(listener); 152 } 153 dispatchOnTabSelected(Tab tab)154 private void dispatchOnTabSelected(Tab tab) { 155 for (Listener listener : mListeners) { 156 listener.onTabSelected(tab); 157 } 158 } 159 dispatchOnTabUnselected(Tab tab)160 private void dispatchOnTabUnselected(Tab tab) { 161 for (Listener listener : mListeners) { 162 listener.onTabUnselected(tab); 163 } 164 } 165 dispatchOnTabReselected(Tab tab)166 private void dispatchOnTabReselected(Tab tab) { 167 for (Listener listener : mListeners) { 168 listener.onTabReselected(tab); 169 } 170 } 171 addTabView(View tabView, int position)172 private void addTabView(View tabView, int position) { 173 addView(tabView, position); 174 } 175 176 private static class TabAdapter extends BaseAdapter { 177 private final Context mContext; 178 private final TabLayout mTabLayout; 179 @LayoutRes 180 private final int mTabItemLayoutRes; 181 private final List<Tab> mTabList; 182 TabAdapter(Context context, @LayoutRes int res, TabLayout tabLayout)183 private TabAdapter(Context context, @LayoutRes int res, TabLayout tabLayout) { 184 mTabList = new ArrayList<>(); 185 mContext = context; 186 mTabItemLayoutRes = res; 187 mTabLayout = tabLayout; 188 } 189 add(@onNull Tab tab)190 private void add(@NonNull Tab tab) { 191 mTabList.add(tab); 192 notifyItemInserted(mTabList.size() - 1); 193 } 194 clear()195 private void clear() { 196 mTabList.clear(); 197 mTabLayout.removeAllViews(); 198 } 199 getPosition(Tab tab)200 private int getPosition(Tab tab) { 201 return mTabList.indexOf(tab); 202 } 203 204 @Override getCount()205 public int getCount() { 206 return mTabList.size(); 207 } 208 209 @Override getItem(int position)210 public Tab getItem(int position) { 211 return mTabList.get(position); 212 } 213 214 @Override getItemId(int position)215 public long getItemId(int position) { 216 return position; 217 } 218 219 @Override 220 @NonNull getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)221 public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { 222 View tabItemView = LayoutInflater.from(mContext) 223 .inflate(mTabItemLayoutRes, parent, false); 224 225 presentTabItemView(position, tabItemView); 226 return tabItemView; 227 } 228 selectTab(Tab tab)229 private void selectTab(Tab tab) { 230 selectTab(getPosition(tab)); 231 } 232 selectTab(int position)233 private void selectTab(int position) { 234 if (position < 0 || position >= getCount()) { 235 throw new IndexOutOfBoundsException("Invalid position"); 236 } 237 238 for (int i = 0; i < getCount(); i++) { 239 Tab tab = mTabList.get(i); 240 boolean isTabSelected = position == i; 241 if (tab.mIsSelected != isTabSelected) { 242 tab.mIsSelected = isTabSelected; 243 notifyItemChanged(i); 244 if (tab.mIsSelected) { 245 mTabLayout.dispatchOnTabSelected(tab); 246 } else { 247 mTabLayout.dispatchOnTabUnselected(tab); 248 } 249 } else if (tab.mIsSelected) { 250 mTabLayout.dispatchOnTabReselected(tab); 251 } 252 } 253 } 254 255 /** Represent the tab item at given position without destroying and recreating UI. */ notifyItemChanged(int position)256 private void notifyItemChanged(int position) { 257 View tabItemView = mTabLayout.getChildAt(position); 258 presentTabItemView(position, tabItemView); 259 } 260 notifyItemInserted(int position)261 private void notifyItemInserted(int position) { 262 View insertedView = getView(position, null, mTabLayout); 263 mTabLayout.addTabView(insertedView, position); 264 } 265 presentTabItemView(int position, @NonNull View tabItemView)266 private void presentTabItemView(int position, @NonNull View tabItemView) { 267 Tab tab = mTabList.get(position); 268 269 ImageView iconView = requireViewByRefId(tabItemView, R.id.car_ui_toolbar_tab_item_icon); 270 TextView textView = requireViewByRefId(tabItemView, R.id.car_ui_toolbar_tab_item_text); 271 272 tabItemView.setOnClickListener(view -> selectTab(tab)); 273 tab.bindText(textView); 274 tab.bindIcon(iconView); 275 tabItemView.setActivated(tab.mIsSelected); 276 textView.setTextAppearance(tab.mIsSelected 277 ? R.style.TextAppearance_CarUi_Widget_Toolbar_Tab_Selected 278 : R.style.TextAppearance_CarUi_Widget_Toolbar_Tab); 279 } 280 } 281 282 /** Tab entity. */ 283 public static class Tab { 284 private final Drawable mIcon; 285 private final CharSequence mText; 286 private boolean mIsSelected; 287 Tab(@ullable Drawable icon, @Nullable CharSequence text)288 public Tab(@Nullable Drawable icon, @Nullable CharSequence text) { 289 mIcon = icon; 290 mText = text; 291 } 292 293 /** Set tab text. */ bindText(TextView textView)294 protected void bindText(TextView textView) { 295 textView.setText(mText); 296 } 297 298 /** Set icon drawable. TODO(b/139444064): revise this api.*/ bindIcon(ImageView imageView)299 protected void bindIcon(ImageView imageView) { 300 imageView.setImageDrawable(mIcon); 301 } 302 } 303 } 304