• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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