Chris Banes dd946bba2e am d972c3ba: am d421b2b3: am f4e0b1dd: Merge "TextInputLayout - a wrapper around EditText" into lmp-mr1-ub-dev
* commit 'd972c3ba1979b8d3710f537de162c362fde0ea30':
  TextInputLayout - a wrapper around EditText

GitOrigin-RevId=809f652c46ec74ffea3505725e577d680a5f47ab

PiperOrigin-RevId: 140545480
2016-12-09 18:12:14 -08:00

1408 lines
50 KiB
Java
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (C) 2015 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 android.support.design.widget;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.IntDef;
import android.support.design.R;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.app.ActionBar;
import android.support.v7.internal.widget.TintManager;
import android.support.v7.widget.AppCompatTextView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Iterator;
import static android.support.design.widget.AnimationUtils.lerp;
/**
* TabLayout provides a horizontal layout to display tabs. <p> Population of the tabs to display is
* done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can
* change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)}
* respectively. To display the tab, you need to add it to the layout via one of the {@link
* #addTab(Tab)} methods. For example:
* <pre>
* TabLayout tabLayout = ...;
* tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
* tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
* tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
* </pre>
* You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be
* notified when any tab's selection state has been changed.
* <p>
* If you're using a {@link android.support.v4.view.ViewPager} together
* with this layout, you can use {@link #addTabsFromPagerAdapter(PagerAdapter)} which will populate
* the tabs using the {@link PagerAdapter}'s page titles. You should also use a {@link
* ViewPager.OnPageChangeListener} to forward the scroll and selection changes to this layout.
* You can use the one returned {@link #createOnPageChangeListener()} for easy implementation:
* <pre>
* ViewPager viewPager = ...;
* TabLayout tabLayout = ...;
* viewPager.setOnPageChangeListener(tabLayout.createOnPageChangeListener());
* </pre>
*
* @see <a href="http://www.google.com/design/spec/components/tabs.html">Tabs</a>
*/
public class TabLayout extends HorizontalScrollView {
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
private static final int MAX_TAB_TEXT_LINES = 2;
private static final int DEFAULT_HEIGHT = 48; // dps
private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps
private static final int FIXED_WRAP_GUTTER_MIN = 16; //dps
private static final int MOTION_NON_ADJACENT_OFFSET = 24;
private static final int ANIMATION_DURATION = 300;
/**
* Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab
* labels and a larger number of tabs. They are best used for browsing contexts in touch
* interfaces when users don’t need to directly compare the tab labels.
*
* @attr android.support.design.R.attr.tabMode
* @see #setTabMode(int)
* @see #getTabMode()
*/
public static final int MODE_SCROLLABLE = 0;
/**
* Fixed tabs display all tabs concurrently and are best used with content that benefits from
* quick pivots between tabs. The maximum number of tabs is limited by the view’s width.
* Fixed tabs have equal width, based on the widest tab label.
*
* @attr android.support.design.R.attr.tabMode
* @see #setTabMode(int)
* @see #getTabMode()
*/
public static final int MODE_FIXED = 1;
/**
* @hide
*/
@IntDef(value = {MODE_SCROLLABLE, MODE_FIXED})
@Retention(RetentionPolicy.SOURCE)
public @interface Mode {}
/**
* Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect
* when used with {@link #MODE_FIXED}.
*
* @attr android.support.design.R.attr.tabGravity
* @see #setTabGravity(int)
* @see #getTabGravity()
*/
public static final int GRAVITY_FILL = 0;
/**
* Gravity used to lay out the tabs in the center of the {@link TabLayout}.
*
* @attr android.support.design.R.attr.tabGravity
* @see #setTabGravity(int)
* @see #getTabGravity()
*/
public static final int GRAVITY_CENTER = 1;
/**
* @hide
*/
@IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER})
@Retention(RetentionPolicy.SOURCE)
public @interface TabGravity {}
/**
* Callback interface invoked when a tab's selection state changes.
*/
public interface OnTabSelectedListener {
/**
* Called when a tab enters the selected state.
*
* @param tab The tab that was selected
*/
public void onTabSelected(Tab tab);
/**
* Called when a tab exits the selected state.
*
* @param tab The tab that was unselected
*/
public void onTabUnselected(Tab tab);
/**
* Called when a tab that is already selected is chosen again by the user. Some applications
* may use this action to return to the top level of a category.
*
* @param tab The tab that was reselected.
*/
public void onTabReselected(Tab tab);
}
private final ArrayList<Tab> mTabs = new ArrayList<>();
private Tab mSelectedTab;
private final SlidingTabStrip mTabStrip;
private int mTabPaddingStart;
private int mTabPaddingTop;
private int mTabPaddingEnd;
private int mTabPaddingBottom;
private final int mTabTextAppearance;
private int mTabSelectedTextColor;
private boolean mTabSelectedTextColorSet;
private final int mTabBackgroundResId;
private final int mTabMinWidth;
private int mTabMaxWidth;
private final int mRequestedTabMaxWidth;
private int mContentInsetStart;
private int mTabGravity;
private int mMode;
private OnTabSelectedListener mOnTabSelectedListener;
private View.OnClickListener mTabClickListener;
public TabLayout(Context context) {
this(context, null);
}
public TabLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// Disable the Scroll Bar
setHorizontalScrollBarEnabled(false);
// Set us to fill the View port
setFillViewport(true);
// Add the TabStrip
mTabStrip = new SlidingTabStrip(context);
addView(mTabStrip, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
defStyleAttr, R.style.Widget_Design_TabLayout);
mTabStrip.setSelectedIndicatorHeight(
a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0));
mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0));
mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance,
R.style.TextAppearance_Design_Tab);
mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a
.getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0);
mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart,
mTabPaddingStart);
mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop,
mTabPaddingTop);
mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd,
mTabPaddingEnd);
mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom,
mTabPaddingBottom);
if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) {
mTabSelectedTextColor = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0);
mTabSelectedTextColorSet = true;
}
mTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, 0);
mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, 0);
mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0);
mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0);
mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED);
mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL);
a.recycle();
// Now apply the tab mode and gravity
applyModeAndGravity();
}
/**
* Set the scroll position of the tabs. This is useful for when the tabs are being displayed as
* part of a scrolling container such as {@link ViewPager}.
* <p>
* Calling this method does not update the selected tab, it is only used for drawing purposes.
*
* @param position current scroll position
* @param positionOffset Value from [0, 1) indicating the offset from {@code position}.
* @param updateSelectedText Whether to update the text's selected state.
*/
public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
if (isAnimationRunning(getAnimation())) {
return;
}
if (position < 0 || position >= mTabStrip.getChildCount()) {
return;
}
// Set the indicator position and update the scroll to match
mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
scrollTo(calculateScrollXForTab(position, positionOffset), 0);
// Update the 'selected state' view as we scroll
if (updateSelectedText) {
setSelectedTabView(Math.round(position + positionOffset));
}
}
/**
* Add new {@link Tab}s populated from a {@link PagerAdapter}. Each tab will have it's text set
* to the value returned from {@link PagerAdapter#getPageTitle(int)}.
*
* @param adapter the adapter to populate from
*/
public void addTabsFromPagerAdapter(PagerAdapter adapter) {
for (int i = 0, count = adapter.getCount(); i < count; i++) {
addTab(newTab().setText(adapter.getPageTitle(i)));
}
}
/**
* Create a {@link ViewPager.OnPageChangeListener} which implements the
* necessary calls back to this layout so that the tabs position is kept in sync.
* <p>
* If you need to have a custom {@link ViewPager.OnPageChangeListener} for your own
* purposes, you can still use the instance returned from this method, but making sure to call
* through to all of the methods.
*/
public ViewPager.OnPageChangeListener createOnPageChangeListener() {
return new ViewPager.OnPageChangeListener() {
private int mScrollState;
@Override
public void onPageScrollStateChanged(int state) {
mScrollState = state;
}
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
// Update the scroll position, only update the text selection if we're being
// dragged
setScrollPosition(position, positionOffset,
mScrollState == ViewPager.SCROLL_STATE_DRAGGING);
}
@Override
public void onPageSelected(int position) {
getTabAt(position).select();
}
};
}
/**
* 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.
*
* @param tab Tab to add
*/
public void addTab(Tab tab) {
addTab(tab, mTabs.isEmpty());
}
/**
* Add a tab to this layout. The tab will be inserted at <code>position</code>.
* If this is the first tab to be added it will become the selected tab.
*
* @param tab The tab to add
* @param position The new position of the tab
*/
public void addTab(Tab tab, int position) {
addTab(tab, position, mTabs.isEmpty());
}
/**
* Add a tab to this layout. The tab will be added at the end of the list.
*
* @param tab Tab to add
* @param setSelected True if the added tab should become the selected tab.
*/
public void addTab(Tab tab, boolean setSelected) {
if (tab.mParent != this) {
throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
}
addTabView(tab, setSelected);
configureTab(tab, mTabs.size());
if (setSelected) {
tab.select();
}
}
/**
* Add a tab to this layout. The tab will be inserted at <code>position</code>.
*
* @param tab The tab to add
* @param position The new position of the tab
* @param setSelected True if the added tab should become the selected tab.
*/
public void addTab(Tab tab, int position, boolean setSelected) {
if (tab.mParent != this) {
throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
}
addTabView(tab, position, setSelected);
configureTab(tab, position);
if (setSelected) {
tab.select();
}
}
/**
* Set the {@link android.support.design.widget.TabLayout.OnTabSelectedListener} that will handle switching to and from tabs.
*
* @param onTabSelectedListener Listener to handle tab selection events
*/
public void setOnTabSelectedListener(OnTabSelectedListener onTabSelectedListener) {
mOnTabSelectedListener = onTabSelectedListener;
}
/**
* Create and return a new {@link Tab}. You need to manually add this using
* {@link #addTab(Tab)} or a related method.
*
* @return A new Tab
* @see #addTab(Tab)
*/
public Tab newTab() {
return new Tab(this);
}
/**
* Returns the number of tabs currently registered with the action bar.
*
* @return Tab count
*/
public int getTabCount() {
return mTabs.size();
}
/**
* Returns the tab at the specified index.
*/
public Tab getTabAt(int index) {
return mTabs.get(index);
}
/**
* Remove a tab from the layout. If the removed tab was selected it will be deselected
* and another tab will be selected if present.
*
* @param tab The tab to remove
*/
public void removeTab(Tab tab) {
if (tab.mParent != this) {
throw new IllegalArgumentException("Tab does not belong to this TabLayout.");
}
removeTabAt(tab.getPosition());
}
/**
* Remove a tab from the layout. If the removed tab was selected it will be deselected
* and another tab will be selected if present.
*
* @param position Position of the tab to remove
*/
public void removeTabAt(int position) {
final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0;
removeTabViewAt(position);
Tab removedTab = mTabs.remove(position);
if (removedTab != null) {
removedTab.setPosition(Tab.INVALID_POSITION);
}
final int newTabCount = mTabs.size();
for (int i = position; i < newTabCount; i++) {
mTabs.get(i).setPosition(i);
}
if (selectedTabPosition == position) {
selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
}
}
/**
* Remove all tabs from the action bar and deselect the current tab.
*/
public void removeAllTabs() {
// Remove all the views
mTabStrip.removeAllViews();
for (Iterator<Tab> i = mTabs.iterator(); i.hasNext(); ) {
Tab tab = i.next();
tab.setPosition(Tab.INVALID_POSITION);
i.remove();
}
}
/**
* Set the behavior mode for the Tabs in this layout. The valid input options are:
* <ul>
* <li>{@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used
* with content that benefits from quick pivots between tabs.</li>
* <li>{@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment,
* and can contain longer tab labels and a larger number of tabs. They are best used for
* browsing contexts in touch interfaces when users don’t need to directly compare the tab
* labels. This mode is commonly used with a {@link ViewPager}.</li>
* </ul>
*
* @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}.
*/
public void setTabMode(@Mode int mode) {
if (mode != mMode) {
mMode = mode;
applyModeAndGravity();
}
}
/**
* Returns the current mode used by this {@link TabLayout}.
*
* @see #setTabMode(int)
*/
@Mode
public int getTabMode() {
return mMode;
}
/**
* Set the gravity to use when laying out the tabs.
*
* @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
*/
public void setTabGravity(@TabGravity int gravity) {
if (mTabGravity != gravity) {
mTabGravity = gravity;
applyModeAndGravity();
}
}
/**
* The current gravity used for laying out tabs.
*
* @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
*/
@TabGravity
public int getTabGravity() {
return mTabGravity;
}
/**
* Set the text color to use when a tab is selected.
*
* @param textColor
*/
public void setTabSelectedTextColor(@ColorInt int textColor) {
if (!mTabSelectedTextColorSet || mTabSelectedTextColor != textColor) {
mTabSelectedTextColor = textColor;
mTabSelectedTextColorSet = true;
for (int i = 0, z = mTabStrip.getChildCount(); i < z; i++) {
updateTab(i);
}
}
}
/**
* Returns the text color currently used when a tab is selected.
*/
@ColorInt
public int getTabSelectedTextColor() {
return mTabSelectedTextColor;
}
private TabView createTabView(Tab tab) {
final TabView tabView = new TabView(getContext(), tab);
tabView.setFocusable(true);
if (mTabClickListener == null) {
mTabClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
TabView tabView = (TabView) view;
tabView.getTab().select();
}
};
}
tabView.setOnClickListener(mTabClickListener);
return tabView;
}
private void configureTab(Tab tab, int position) {
tab.setPosition(position);
mTabs.add(position, tab);
final int count = mTabs.size();
for (int i = position + 1; i < count; i++) {
mTabs.get(i).setPosition(i);
}
}
private void updateTab(int position) {
final TabView view = (TabView) mTabStrip.getChildAt(position);
if (view != null) {
view.update();
}
}
private void addTabView(Tab tab, boolean setSelected) {
final TabView tabView = createTabView(tab);
mTabStrip.addView(tabView, createLayoutParamsForTabs());
if (setSelected) {
tabView.setSelected(true);
}
}
private void addTabView(Tab tab, int position, boolean setSelected) {
final TabView tabView = createTabView(tab);
mTabStrip.addView(tabView, position, createLayoutParamsForTabs());
if (setSelected) {
tabView.setSelected(true);
}
}
private LinearLayout.LayoutParams createLayoutParamsForTabs() {
final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
updateTabViewLayoutParams(lp);
return lp;
}
private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
lp.width = 0;
lp.weight = 1;
} else {
lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
lp.weight = 0;
}
}
private int dpToPx(int dps) {
return Math.round(getResources().getDisplayMetrics().density * dps);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// If we have a MeasureSpec which allows us to decide our height, try and use the default
// height
switch (MeasureSpec.getMode(heightMeasureSpec)) {
case MeasureSpec.AT_MOST:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.min(dpToPx(DEFAULT_HEIGHT), MeasureSpec.getSize(heightMeasureSpec)),
MeasureSpec.EXACTLY);
break;
case MeasureSpec.UNSPECIFIED:
heightMeasureSpec = MeasureSpec.makeMeasureSpec(dpToPx(DEFAULT_HEIGHT),
MeasureSpec.EXACTLY);
break;
}
// Now super measure itself using the (possibly) modified height spec
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mMode == MODE_FIXED && getChildCount() == 1) {
// If we're in fixed mode then we need to make the tab strip is the same width as us
// so we don't scroll
final View child = getChildAt(0);
final int width = getMeasuredWidth();
if (child.getMeasuredWidth() > width) {
// If the child is wider than us, re-measure it with a widthSpec set to exact our
// measure width
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop()
+ getPaddingBottom(), child.getLayoutParams().height);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
// Now update the tab max width. We do it here as the default tab min width is
// layout width - 56dp
int maxTabWidth = mRequestedTabMaxWidth;
final int defaultTabMaxWidth = getMeasuredWidth() - dpToPx(TAB_MIN_WIDTH_MARGIN);
if (maxTabWidth == 0 || maxTabWidth > defaultTabMaxWidth) {
// If the request tab max width is 0, or larger than our default, use the default
maxTabWidth = defaultTabMaxWidth;
}
mTabMaxWidth = maxTabWidth;
}
private void removeTabViewAt(int position) {
mTabStrip.removeViewAt(position);
requestLayout();
}
private void animateToTab(int newPosition) {
clearAnimation();
if (newPosition == Tab.INVALID_POSITION) {
return;
}
if (getWindowToken() == null || !ViewCompat.isLaidOut(this)) {
// If we don't have a window token, or we haven't been laid out yet just draw the new
// position now
setScrollPosition(newPosition, 0f, true);
return;
}
final int startScrollX = getScrollX();
final int targetScrollX = calculateScrollXForTab(newPosition, 0);
final int duration = ANIMATION_DURATION;
if (startScrollX != targetScrollX) {
final Animation animation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float value = lerp(startScrollX, targetScrollX, interpolatedTime);
scrollTo((int) value, 0);
}
};
animation.setInterpolator(INTERPOLATOR);
animation.setDuration(duration);
startAnimation(animation);
}
// Now animate the indicator
mTabStrip.animateIndicatorToPosition(newPosition, duration);
}
private void setSelectedTabView(int position) {
final int tabCount = mTabStrip.getChildCount();
for (int i = 0; i < tabCount; i++) {
final View child = mTabStrip.getChildAt(i);
child.setSelected(i == position);
}
}
private static boolean isAnimationRunning(Animation animation) {
return animation != null && animation.hasStarted() && !animation.hasEnded();
}
void selectTab(Tab tab) {
if (mSelectedTab == tab) {
if (mSelectedTab != null) {
if (mOnTabSelectedListener != null) {
mOnTabSelectedListener.onTabReselected(mSelectedTab);
}
animateToTab(tab.getPosition());
}
} else {
final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION;
setSelectedTabView(newPosition);
if ((mSelectedTab == null || mSelectedTab.getPosition() == Tab.INVALID_POSITION)
&& newPosition != Tab.INVALID_POSITION) {
// If we don't currently have a tab, just draw the indicator
setScrollPosition(newPosition, 0f, true);
} else {
animateToTab(newPosition);
}
if (mSelectedTab != null && mOnTabSelectedListener != null) {
mOnTabSelectedListener.onTabUnselected(mSelectedTab);
}
mSelectedTab = tab;
if (mSelectedTab != null && mOnTabSelectedListener != null) {
mOnTabSelectedListener.onTabSelected(mSelectedTab);
}
}
}
private int calculateScrollXForTab(int position, float positionOffset) {
if (mMode == MODE_SCROLLABLE) {
final View selectedChild = mTabStrip.getChildAt(position);
final View nextChild = position + 1 < mTabStrip.getChildCount()
? mTabStrip.getChildAt(position + 1)
: null;
final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
return (int) (selectedChild.getLeft()
+ ((selectedWidth + nextWidth) * positionOffset * 0.5f)
+ selectedChild.getWidth() * 0.5f
- getWidth() * 0.5f);
}
return 0;
}
private void applyModeAndGravity() {
int paddingStart = 0;
if (mMode == MODE_SCROLLABLE) {
// If we're scrollable, or fixed at start, inset using padding
paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
}
ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);
switch (mMode) {
case MODE_FIXED:
mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
break;
case MODE_SCROLLABLE:
mTabStrip.setGravity(GravityCompat.START);
break;
}
updateTabViewsLayoutParams();
}
private void updateTabViewsLayoutParams() {
for (int i = 0; i < mTabStrip.getChildCount(); i++) {
View child = mTabStrip.getChildAt(i);
updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
child.requestLayout();
}
}
/**
* A tab in this layout. Instances can be created via {@link #newTab()}.
*/
public static final class Tab {
/**
* An invalid position for a tab.
*
* @see #getPosition()
*/
public static final int INVALID_POSITION = -1;
private Object mTag;
private Drawable mIcon;
private CharSequence mText;
private CharSequence mContentDesc;
private int mPosition = INVALID_POSITION;
private View mCustomView;
private final TabLayout mParent;
Tab(TabLayout parent) {
mParent = parent;
}
/**
* @return This Tab's tag object.
*/
public Object getTag() {
return mTag;
}
/**
* Give this Tab an arbitrary object to hold for later use.
*
* @param tag Object to store
* @return The current instance for call chaining
*/
public Tab setTag(Object tag) {
mTag = tag;
return this;
}
View getCustomView() {
return mCustomView;
}
/**
* Set a custom view to be used for this tab. This overrides values set by {@link
* #setText(CharSequence)} and {@link #setIcon(Drawable)}.
*
* @param view Custom view to be used as a tab.
* @return The current instance for call chaining
*/
public Tab setCustomView(View view) {
mCustomView = view;
if (mPosition >= 0) {
mParent.updateTab(mPosition);
}
return this;
}
/**
* Set a custom view to be used for this tab. This overrides values set by {@link
* #setText(CharSequence)} and {@link #setIcon(Drawable)}.
*
* @param layoutResId A layout resource to inflate and use as a custom tab view
* @return The current instance for call chaining
*/
public Tab setCustomView(int layoutResId) {
return setCustomView(
LayoutInflater.from(mParent.getContext()).inflate(layoutResId, null));
}
/**
* Return the icon associated with this tab.
*
* @return The tab's icon
*/
public Drawable getIcon() {
return mIcon;
}
/**
* Return the current position of this tab in the action bar.
*
* @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
* the action bar.
*/
public int getPosition() {
return mPosition;
}
void setPosition(int position) {
mPosition = position;
}
/**
* Return the text of this tab.
*
* @return The tab's text
*/
public CharSequence getText() {
return mText;
}
/**
* Set the icon displayed on this tab.
*
* @param icon The drawable to use as an icon
* @return The current instance for call chaining
*/
public Tab setIcon(Drawable icon) {
mIcon = icon;
if (mPosition >= 0) {
mParent.updateTab(mPosition);
}
return this;
}
/**
* Set the icon displayed on this tab.
*
* @param resId A resource ID referring to the icon that should be displayed
* @return The current instance for call chaining
*/
public Tab setIcon(int resId) {
return setIcon(TintManager.getDrawable(mParent.getContext(), resId));
}
/**
* Set the text displayed on this tab. Text may be truncated if there is not room to display
* the entire string.
*
* @param text The text to display
* @return The current instance for call chaining
*/
public Tab setText(CharSequence text) {
mText = text;
if (mPosition >= 0) {
mParent.updateTab(mPosition);
}
return this;
}
/**
* Set the text displayed on this tab. Text may be truncated if there is not room to display
* the entire string.
*
* @param resId A resource ID referring to the text that should be displayed
* @return The current instance for call chaining
*/
public Tab setText(int resId) {
return setText(mParent.getResources().getText(resId));
}
/**
* Select this tab. Only valid if the tab has been added to the action bar.
*/
public void select() {
mParent.selectTab(this);
}
/**
* Set a description of this tab's content for use in accessibility support. If no content
* description is provided the title will be used.
*
* @param resId A resource ID referring to the description text
* @return The current instance for call chaining
* @see #setContentDescription(CharSequence)
* @see #getContentDescription()
*/
public Tab setContentDescription(int resId) {
return setContentDescription(mParent.getResources().getText(resId));
}
/**
* Set a description of this tab's content for use in accessibility support. If no content
* description is provided the title will be used.
*
* @param contentDesc Description of this tab's content
* @return The current instance for call chaining
* @see #setContentDescription(int)
* @see #getContentDescription()
*/
public Tab setContentDescription(CharSequence contentDesc) {
mContentDesc = contentDesc;
if (mPosition >= 0) {
mParent.updateTab(mPosition);
}
return this;
}
/**
* Gets a brief description of this tab's content for use in accessibility support.
*
* @return Description of this tab's content
* @see #setContentDescription(CharSequence)
* @see #setContentDescription(int)
*/
public CharSequence getContentDescription() {
return mContentDesc;
}
}
class TabView extends LinearLayout implements OnLongClickListener {
private final Tab mTab;
private TextView mTextView;
private ImageView mIconView;
private View mCustomView;
public TabView(Context context, Tab tab) {
super(context);
mTab = tab;
if (mTabBackgroundResId != 0) {
setBackgroundDrawable(TintManager.getDrawable(context, mTabBackgroundResId));
}
ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
mTabPaddingEnd, mTabPaddingBottom);
setGravity(Gravity.CENTER);
update();
}
@Override
public void setSelected(boolean selected) {
final boolean changed = (isSelected() != selected);
super.setSelected(selected);
if (changed && selected) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
if (mTextView != null) {
mTextView.setSelected(selected);
}
if (mIconView != null) {
mIconView.setSelected(selected);
}
}
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
// This view masquerades as an action bar tab.
event.setClassName(ActionBar.Tab.class.getName());
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
// This view masquerades as an action bar tab.
info.setClassName(ActionBar.Tab.class.getName());
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getMeasuredWidth() > mTabMaxWidth) {
// Re-measure if we went beyond our maximum size.
super.onMeasure(MeasureSpec.makeMeasureSpec(
mTabMaxWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
} else if (mTabMinWidth > 0 && getMeasuredHeight() < mTabMinWidth) {
// Re-measure if we're below our minimum size.
super.onMeasure(MeasureSpec.makeMeasureSpec(
mTabMinWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
}
}
final void update() {
final Tab tab = mTab;
final View custom = tab.getCustomView();
if (custom != null) {
final ViewParent customParent = custom.getParent();
if (customParent != this) {
if (customParent != null) {
((ViewGroup) customParent).removeView(custom);
}
addView(custom);
}
mCustomView = custom;
if (mTextView != null) {
mTextView.setVisibility(GONE);
}
if (mIconView != null) {
mIconView.setVisibility(GONE);
mIconView.setImageDrawable(null);
}
} else {
if (mCustomView != null) {
removeView(mCustomView);
mCustomView = null;
}
final Drawable icon = tab.getIcon();
final CharSequence text = tab.getText();
if (icon != null) {
if (mIconView == null) {
ImageView iconView = new ImageView(getContext());
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
lp.gravity = Gravity.CENTER_VERTICAL;
iconView.setLayoutParams(lp);
addView(iconView, 0);
mIconView = iconView;
}
mIconView.setImageDrawable(icon);
mIconView.setVisibility(VISIBLE);
} else if (mIconView != null) {
mIconView.setVisibility(GONE);
mIconView.setImageDrawable(null);
}
final boolean hasText = !TextUtils.isEmpty(text);
if (hasText) {
if (mTextView == null) {
AppCompatTextView textView = new AppCompatTextView(getContext());
textView.setTextAppearance(getContext(), mTabTextAppearance);
textView.setMaxLines(MAX_TAB_TEXT_LINES);
textView.setEllipsize(TextUtils.TruncateAt.END);
textView.setGravity(Gravity.CENTER);
if (mTabSelectedTextColorSet) {
textView.setTextColor(createColorStateList(
textView.getCurrentTextColor(), mTabSelectedTextColor));
}
addView(textView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
mTextView = textView;
}
mTextView.setText(text);
mTextView.setVisibility(VISIBLE);
} else if (mTextView != null) {
mTextView.setVisibility(GONE);
mTextView.setText(null);
}
if (mIconView != null) {
mIconView.setContentDescription(tab.getContentDescription());
}
if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) {
setOnLongClickListener(this);
} else {
setOnLongClickListener(null);
setLongClickable(false);
}
}
}
@Override
public boolean onLongClick(View v) {
final int[] screenPos = new int[2];
getLocationOnScreen(screenPos);
final Context context = getContext();
final int width = getWidth();
final int height = getHeight();
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(),
Toast.LENGTH_SHORT);
// Show under the tab
cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
(screenPos[0] + width / 2) - screenWidth / 2, height);
cheatSheet.show();
return true;
}
private ColorStateList createColorStateList(int defaultColor, int selectedColor) {
final int[][] states = new int[2][];
final int[] colors = new int[2];
int i = 0;
states[i] = SELECTED_STATE_SET;
colors[i] = selectedColor;
i++;
// Default enabled state
states[i] = EMPTY_STATE_SET;
colors[i] = defaultColor;
i++;
return new ColorStateList(states, colors);
}
public Tab getTab() {
return mTab;
}
}
private class SlidingTabStrip extends LinearLayout {
private int mSelectedIndicatorHeight;
private final Paint mSelectedIndicatorPaint;
private int mSelectedPosition = -1;
private float mSelectionOffset;
private int mIndicatorLeft = -1;
private int mIndicatorRight = -1;
SlidingTabStrip(Context context) {
super(context);
setWillNotDraw(false);
mSelectedIndicatorPaint = new Paint();
}
void setSelectedIndicatorColor(int color) {
mSelectedIndicatorPaint.setColor(color);
ViewCompat.postInvalidateOnAnimation(this);
}
void setSelectedIndicatorHeight(int height) {
mSelectedIndicatorHeight = height;
ViewCompat.postInvalidateOnAnimation(this);
}
void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
if (isAnimationRunning(getAnimation())) {
return;
}
mSelectedPosition = position;
mSelectionOffset = positionOffset;
updateIndicatorPosition();
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
// HorizontalScrollView will first measure use with UNSPECIFIED, and then with
// EXACTLY. Ignore the first call since anything we do will be overwritten anyway
return;
}
if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
final int count = getChildCount();
final int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
// First we'll find the largest tab
int largestTabWidth = 0;
for (int i = 0, z = count; i < z; i++) {
final View child = getChildAt(i);
child.measure(unspecifiedSpec, heightMeasureSpec);
largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
}
if (largestTabWidth <= 0) {
// If we don't have a largest child yet, skip until the next measure pass
return;
}
final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
// If the tabs fit within our width minus gutters, we will set all tabs to have
// the same width
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final LinearLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.width = largestTabWidth;
lp.weight = 0;
}
} else {
// If the tabs will wrap to be larger than the width minus gutters, we need
// to switch to GRAVITY_FILL
mTabGravity = GRAVITY_FILL;
updateTabViewsLayoutParams();
}
// Now re-measure after our changes
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (!isAnimationRunning(getAnimation())) {
// If we've been layed out, and we're not currently in an animation, update the
// indicator position
updateIndicatorPosition();
}
}
private void updateIndicatorPosition() {
final View selectedTitle = getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
left = selectedTitle.getLeft();
right = selectedTitle.getRight();
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
// Draw the selection partway between the tabs
View nextTitle = getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * nextTitle.getLeft() +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextTitle.getRight() +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
private void setIndicatorPosition(int left, int right) {
if (left != mIndicatorLeft || right != mIndicatorRight) {
// If the indicator's left/right has changed, invalidate
mIndicatorLeft = left;
mIndicatorRight = right;
ViewCompat.postInvalidateOnAnimation(this);
}
}
void animateIndicatorToPosition(final int position, int duration) {
final boolean isRtl = ViewCompat.getLayoutDirection(this)
== ViewCompat.LAYOUT_DIRECTION_RTL;
final View targetView = getChildAt(position);
final int targetLeft = targetView.getLeft();
final int targetRight = targetView.getRight();
final int startLeft;
final int startRight;
if (Math.abs(position - mSelectedPosition) <= 1) {
// If the views are adjacent, we'll animate from edge-to-edge
startLeft = mIndicatorLeft;
startRight = mIndicatorRight;
} else {
// Else, we'll just grow from the nearest edge
final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
if (position < mSelectedPosition) {
// We're going end-to-start
if (isRtl) {
startLeft = startRight = targetLeft - offset;
} else {
startLeft = startRight = targetRight + offset;
}
} else {
// We're going start-to-end
if (isRtl) {
startLeft = startRight = targetRight + offset;
} else {
startLeft = startRight = targetLeft - offset;
}
}
}
if (startLeft != targetLeft || startRight != targetRight) {
final Animation anim = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
setIndicatorPosition(
(int) lerp(startLeft, targetLeft, interpolatedTime),
(int) lerp(startRight, targetRight, interpolatedTime));
}
};
anim.setInterpolator(INTERPOLATOR);
anim.setDuration(duration);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
mSelectedPosition = position;
mSelectionOffset = 0f;
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
startAnimation(anim);
}
}
@Override
protected void onDraw(Canvas canvas) {
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
}
}