blob: 4dd427f8a56aec09a8556d55d7b6603bdd808dec [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.apps.common.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import com.android.car.apps.common.R;
import com.android.car.apps.common.util.Themes;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Custom tab layout which supports adding tabs dynamically
*
* <p>It supports two layout modes:
* <ul><li>Flexible layout which will fill the width
* <li>Non-flexible layout which wraps content with a minimum tab width. By setting tab gravity,
* it can left aligned, right aligned or center aligned.
*
* <p>Scrolling function is not supported. If a tab item runs out of the tab layout bound, there
* is no way to access it. It's better to set the layout mode to flexible in this case.
*
* <p>Default tab item inflates from R.layout.car_tab_item, but it also supports custom layout id.
* By doing this, appearance of tab item view can be customized.
*
* <p>Touch feedback is using @android:attr/selectableItemBackground.
*
* @param <T> Presents a CarTab entity
* @deprecated Use {@link com.android.car.ui.TabLayout} instead
*/
@Deprecated
public class CarTabLayout<T extends CarTabLayout.CarTab> extends LinearLayout {
/**
* Listener that listens the car tab selection change.
*
* @param <T> Presents a CarTab entity that has state update on a tab select action
*/
public interface OnCarTabSelectedListener<T extends CarTab> {
/** Callback triggered when a car tab is selected. */
void onCarTabSelected(T carTab);
/** Callback triggered when a car tab is unselected. */
void onCarTabUnselected(T carTab);
/** Callback triggered when a car tab is reselected. */
void onCarTabReselected(T carTab);
}
/**
* No-op implementation of {@link OnCarTabSelectedListener}.
*
* @param <T> See {@link OnCarTabSelectedListener}
*/
public static class SimpleOnCarTabSelectedListener<T extends CarTab> implements
OnCarTabSelectedListener<T> {
@Override
public void onCarTabSelected(T carTab) {
// No-op
}
@Override
public void onCarTabUnselected(T carTab) {
// No-op
}
@Override
public void onCarTabReselected(T carTab) {
// No-op
}
}
// View attributes
private final boolean mTabFlexibleLayout;
private final int mTabPaddingX;
private final Set<OnCarTabSelectedListener<T>> mOnCarTabSelectedListeners;
private final CarTabAdapter<T> mCarTabAdapter;
public CarTabLayout(@NonNull Context context) {
this(context, null);
}
public CarTabLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CarTabLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mOnCarTabSelectedListeners = new ArraySet<>();
TypedArray ta = context.obtainStyledAttributes(
attrs, R.styleable.CarTabLayout, defStyle, 0);
mTabPaddingX = ta.getDimensionPixelSize(R.styleable.CarTabLayout_tabPaddingX,
context.getResources().getDimensionPixelSize(R.dimen.car_tab_padding_x));
mTabFlexibleLayout = ta.getBoolean(R.styleable.CarTabLayout_tabFlexibleLayout,
context.getResources().getBoolean(R.bool.car_tab_flexible_layout));
int tabItemLayout = ta.getResourceId(R.styleable.CarTabLayout_tabItemLayout,
R.layout.car_tab_item);
ta.recycle();
mCarTabAdapter = new CarTabAdapter(context, tabItemLayout, this);
}
/**
* Add a tab to this layout. The tab will be added at the end of the list. If this is the first
* tab to be added it will become the selected tab.
*/
public void addCarTab(T carTab) {
mCarTabAdapter.add(carTab);
// If there is only one tab in the group, set it to be selected.
if (mCarTabAdapter.getCount() == 1) {
mCarTabAdapter.selectCarTab(0);
}
}
/** Set the tab as the current selected tab. */
public void selectCarTab(T carTab) {
mCarTabAdapter.selectCarTab(carTab);
}
/** Set the tab at given position as the current selected tab. */
public void selectCarTab(int position) {
mCarTabAdapter.selectCarTab(position);
}
/** Returns how tab items it has. */
public int getCarTabCount() {
return mCarTabAdapter.getCount();
}
/** Returns the position of the given car tab. */
public int getCarTabPosition(T carTab) {
return mCarTabAdapter.getPosition(carTab);
}
/** Return the car tab at the given position. */
public T get(int position) {
return mCarTabAdapter.getItem(position);
}
/** Clear all car tabs. */
public void clearAllCarTabs() {
mCarTabAdapter.clear();
}
/** Register a {@link OnCarTabSelectedListener}. Same listener will only be registered once. */
public void addOnCarTabSelectedListener(
@NonNull OnCarTabSelectedListener onCarTabSelectedListener) {
mOnCarTabSelectedListeners.add(onCarTabSelectedListener);
}
/** Unregister a {@link OnCarTabSelectedListener} */
public void removeOnCarTabSelectedListener(
@NonNull OnCarTabSelectedListener onCarTabSelectedListener) {
mOnCarTabSelectedListeners.remove(onCarTabSelectedListener);
}
private void dispatchOnCarTabSelected(T carTab) {
for (OnCarTabSelectedListener onCarTabSelectedListener : mOnCarTabSelectedListeners) {
onCarTabSelectedListener.onCarTabSelected(carTab);
}
}
private void dispatchOnCarTabUnselected(T carTab) {
for (OnCarTabSelectedListener onCarTabSelectedListener : mOnCarTabSelectedListeners) {
onCarTabSelectedListener.onCarTabUnselected(carTab);
}
}
private void dispatchOnCarTabReselected(T carTab) {
for (OnCarTabSelectedListener onCarTabSelectedListener : mOnCarTabSelectedListeners) {
onCarTabSelectedListener.onCarTabReselected(carTab);
}
}
private void addCarTabView(View carTabView, int position) {
LayoutParams layoutParams;
if (mTabFlexibleLayout) {
layoutParams = new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.weight = 1;
} else {
layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
addView(carTabView, position, layoutParams);
}
private ViewGroup createCarTabItemView() {
LinearLayout carTabItemView = new LinearLayout(mContext);
carTabItemView.setOrientation(LinearLayout.VERTICAL);
carTabItemView.setGravity(Gravity.CENTER);
carTabItemView.setPadding(mTabPaddingX, 0, mTabPaddingX, 0);
Drawable backgroundDrawable = Themes.getAttrDrawable(getContext(),
R.style.CarTabItemBackground, android.R.attr.background);
carTabItemView.setBackground(backgroundDrawable);
return carTabItemView;
}
private static class CarTabAdapter<T extends CarTab> extends BaseAdapter {
private final Context mContext;
private final CarTabLayout mCarTabLayout;
@LayoutRes
private final int mCarTabItemLayoutRes;
private final Typeface mUnselectedTypeface;
private final Typeface mSelectedTypeface;
private final List<T> mCarTabList;
private CarTabAdapter(Context context, @LayoutRes int res, CarTabLayout carTabLayout) {
mCarTabList = new ArrayList<>();
mContext = context;
mCarTabItemLayoutRes = res;
mCarTabLayout = carTabLayout;
mUnselectedTypeface = createStyledTypeface(context, R.style.CarTabItemText);
mSelectedTypeface = createStyledTypeface(context, R.style.CarTabSelectedTextTypeface);
}
private void add(@NonNull T carTab) {
mCarTabList.add(carTab);
notifyItemInserted(mCarTabList.size() - 1);
}
private void clear() {
mCarTabList.clear();
mCarTabLayout.removeAllViews();
}
private int getPosition(CarTab carTab) {
return mCarTabList.indexOf(carTab);
}
@Override
public int getCount() {
return mCarTabList.size();
}
@Override
public T getItem(int position) {
return mCarTabList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
@NonNull
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ViewGroup carTabItemView = mCarTabLayout.createCarTabItemView();
LayoutInflater.from(mContext).inflate(mCarTabItemLayoutRes, carTabItemView, true);
presentCarTabItemView(position, carTabItemView);
return carTabItemView;
}
private void selectCarTab(CarTab carTab) {
selectCarTab(getPosition(carTab));
}
private void selectCarTab(int position) {
if (position < 0 || position >= getCount()) {
throw new IndexOutOfBoundsException("Invalid position");
}
for (int i = 0; i < getCount(); i++) {
CarTab carTabItem = mCarTabList.get(i);
boolean isTabSelected = position == i;
if (carTabItem.mIsSelected != isTabSelected) {
carTabItem.mIsSelected = isTabSelected;
notifyItemChanged(i);
if (carTabItem.mIsSelected) {
mCarTabLayout.dispatchOnCarTabSelected(carTabItem);
} else {
mCarTabLayout.dispatchOnCarTabUnselected(carTabItem);
}
} else if (carTabItem.mIsSelected) {
mCarTabLayout.dispatchOnCarTabReselected(carTabItem);
}
}
}
/** Represent the car tab item at given position without destroying and recreating UI. */
private void notifyItemChanged(int position) {
View carTabItemView = mCarTabLayout.getChildAt(position);
presentCarTabItemView(position, carTabItemView);
}
private void notifyItemInserted(int position) {
View insertedView = getView(position, null, mCarTabLayout);
mCarTabLayout.addCarTabView(insertedView, position);
}
private void presentCarTabItemView(int position, @NonNull View carTabItemView) {
CarTab carTab = mCarTabList.get(position);
ImageView iconView = carTabItemView.findViewById(R.id.car_tab_item_icon);
TextView textView = carTabItemView.findViewById(R.id.car_tab_item_text);
carTabItemView.setOnClickListener(view -> selectCarTab(carTab));
carTab.bindText(textView);
carTab.bindIcon(iconView);
carTabItemView.setSelected(carTab.mIsSelected);
iconView.setSelected(carTab.mIsSelected);
textView.setSelected(carTab.mIsSelected);
textView.setTypeface(carTab.mIsSelected ? mSelectedTypeface : mUnselectedTypeface);
}
private static Typeface createStyledTypeface(Context context, @StyleRes int styleResId) {
// If not specified, default to 0, which stands for normal.
int textStyle = Themes.getAttrInteger(context, styleResId, android.R.attr.textStyle);
// If not specified, default value will be 0 which is a light font.
int textFontWeight = Themes.getAttrInteger(context, styleResId,
android.R.attr.textFontWeight);
return Typeface.create(Typeface.defaultFromStyle(textStyle), textFontWeight,
(textStyle & Typeface.ITALIC) != 0);
}
}
/** Car tab entity. */
public static class CarTab {
private final Drawable mIcon;
private final CharSequence mText;
private boolean mIsSelected;
public CarTab(@Nullable Drawable icon, @Nullable CharSequence text) {
mIcon = icon;
mText = text;
}
/** Set tab text. */
protected void bindText(TextView textView) {
textView.setText(mText);
}
/** Set icon drawable. TODO(b/139444064): revise this api. */
protected void bindIcon(ImageView imageView) {
imageView.setImageDrawable(mIcon);
}
}
}