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