Java tutorial
/* * 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 dev.journey.uitoolkit.view; import android.animation.Animator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; 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.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; 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.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.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import dev.journey.uitoolkit.util.AnimationUtils; public class FlexibleTabLayout extends HorizontalScrollView { int mCustomViewLayoutId; 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; public static final int MODE_SCROLLABLE = 0; public static final int MODE_FIXED = 1; /** * @hide */ @IntDef(value = { MODE_SCROLLABLE, MODE_FIXED }) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } public static final int GRAVITY_FILL = 0; 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 ColorStateList mTabTextColors; private int mTabMaxWidth = Integer.MAX_VALUE; private int mTabGravity; private OnTabSelectedListener mOnTabSelectedListener; private OnClickListener mTabClickListener; private ValueAnimator mScrollAnimator; private ValueAnimator mIndicatorAnimator; public FlexibleTabLayout(Context context) { this(context, null); } public FlexibleTabLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FlexibleTabLayout(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.MATCH_PARENT, LayoutParams.MATCH_PARENT); } /** * Sets the tab indicator's color for the currently selected tab. * * @param color color to use for the indicator */ public void setSelectedTabIndicatorColor(@ColorInt int color) { mTabStrip.setSelectedIndicatorColor(color); } /** * Sets the tab indicator's height for the currently selected tab. * * @param height height to use for the indicator in pixels */ public void setSelectedTabIndicatorHeight(int height) { mTabStrip.setSelectedIndicatorHeight(height); } private float getScrollPosition() { return mTabStrip.getIndicatorPosition(); } /** * 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(@NonNull Tab tab) { addTab(tab, false); } /** * 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(@NonNull 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(@NonNull Tab tab, boolean setSelected) { if (tab.mParent != this) { throw new IllegalArgumentException("Tab belongs to a different FlexibleTabLayout."); } 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(@NonNull Tab tab, int position, boolean setSelected) { if (tab.mParent != this) { throw new IllegalArgumentException("Tab belongs to a different FlexibleTabLayout."); } addTabView(tab, position, setSelected); configureTab(tab, position); if (setSelected) { tab.select(); } } 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) */ @NonNull 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. */ @Nullable public Tab getTabAt(int index) { return mTabs.get(index); } /** * Returns the position of the current selected tab. * * @return selected tab position, or {@code -1} if there isn't a selected tab. */ public int getSelectedTabPosition() { return mSelectedTab != null ? mSelectedTab.getPosition() : -1; } /** * 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 FlexibleTabLayout."); } 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(); } mSelectedTab = null; } /** * Sets the text colors for the different states (normal, selected) used for the tabs. */ public void setTabTextColors(@Nullable ColorStateList textColor) { if (mTabTextColors != textColor) { mTabTextColors = textColor; updateAllTabs(); } } /** * Gets the text colors for the different states (normal, selected) used for the tabs. */ @Nullable public ColorStateList getTabTextColors() { return mTabTextColors; } /** * Sets the text colors for the different states (normal, selected) used for the tabs. */ public void setTabTextColors(int normalColor, int selectedColor) { setTabTextColors(createColorStateList(normalColor, selectedColor)); } /** * The one-stop shop for setting up this {@link FlexibleTabLayout} with a {@link ViewPager}. * <p/> * <p>This method will: * <ul> * <li>Add a {@link ViewPager.OnPageChangeListener} that will forward events to * this FlexibleTabLayout.</li> * <li>Populate the FlexibleTabLayout's tabs from the ViewPager's {@link PagerAdapter}.</li> * <li>Set our {@link FlexibleTabLayout.OnTabSelectedListener} which will forward * selected events to the ViewPager</li> * </ul> * </p> * * @see #setTabsFromPagerAdapter(PagerAdapter) * @see FlexibleTabLayoutOnPageChangeListener * @see ViewPagerOnTabSelectedListener */ public void setupWithViewPager(@NonNull ViewPager viewPager, int layoutResId, OnTabSelectedListener listener) { final PagerAdapter adapter = viewPager.getAdapter(); mCustomViewLayoutId = layoutResId; if (adapter == null) { throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set"); } // Now we'll add a tab selected listener to set ViewPager's current item setOnTabSelectedListener(listener); // First we'll add Tabs, using the adapter's page titles setTabsFromPagerAdapter(adapter); // Now we'll add our page change listener to the ViewPager viewPager.addOnPageChangeListener(new FlexibleTabLayoutOnPageChangeListener(this)); // Make sure we reflect the currently set ViewPager item if (adapter.getCount() > 0) { final int curItem = viewPager.getCurrentItem(); if (getSelectedTabPosition() != curItem) { selectTab(getTabAt(curItem)); } } } /** * Populate our tab content from the given {@link PagerAdapter}. * <p> * Any existing tabs will be removed first. Each tab will have it's text set to the value * returned from {@link PagerAdapter#getPageTitle(int)} * </p> * * @param adapter the adapter to populate from */ public void setTabsFromPagerAdapter(@NonNull PagerAdapter adapter) { removeAllTabs(); for (int i = 0, count = adapter.getCount(); i < count; i++) { Tab tab = newTab(); if (mCustomViewLayoutId != 0) { tab.setCustomView(mCustomViewLayoutId); } addTab(tab.setText(adapter.getPageTitle(i)), false); } if (getTabCount() > 0) { getTabAt(0).select(); } } private void updateAllTabs() { for (int i = 0, z = mTabStrip.getChildCount(); i < z; i++) { updateTab(i); } } private TabView createTabView(Tab tab) { final TabView tabView = new TabView(getContext(), tab); tabView.setFocusable(true); if (mTabClickListener == null) { mTabClickListener = new 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.MATCH_PARENT, LayoutParams.MATCH_PARENT); updateTabViewLayoutParams(lp); return lp; } private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { lp.width = 0; lp.weight = 1; } private int dpToPx(int dps) { return Math.round(getResources().getDisplayMetrics().density * dps); } private void removeTabViewAt(int position) { mTabStrip.removeViewAt(position); requestLayout(); } private void animateToTab(int newPosition) { } private void setSelectedTabView(int position) { final int tabCount = mTabStrip.getChildCount(); if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) { for (int i = 0; i < tabCount; i++) { final View child = mTabStrip.getChildAt(i); child.setSelected(i == position); } } } void selectTab(Tab tab) { selectTab(tab, true); } void selectTab(Tab tab, boolean updateIndicator) { 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 && mOnTabSelectedListener != null) { mOnTabSelectedListener.onTabUnselected(mSelectedTab); } mSelectedTab = tab; if (mSelectedTab != null && mOnTabSelectedListener != null) { mOnTabSelectedListener.onTabSelected(mSelectedTab); } } } 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 FlexibleTabLayout mParent; Tab(FlexibleTabLayout parent) { mParent = parent; } /** * @return This Tab's tag object. */ @Nullable 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 */ @NonNull public Tab setTag(@Nullable Object tag) { mTag = tag; return this; } /** * Returns the custom view used for this tab. * * @see #setCustomView(View) * @see #setCustomView(int) */ @Nullable public View getCustomView() { return mCustomView; } /** * Set a custom view to be used for this tab. * <p> * If the provided view contains a {@link TextView} with an ID of * {@link android.R.id#text1} then that will be updated with the value given * to {@link #setText(CharSequence)}. Similarly, if this layout contains an * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with * the value given to {@link #setIcon(Drawable)}. * </p> * * @param view Custom view to be used as a tab. * @return The current instance for call chaining */ @NonNull public Tab setCustomView(@Nullable View view) { mCustomView = view; if (mPosition >= 0) { mParent.updateTab(mPosition); } return this; } /** * Set a custom view to be used for this tab. * <p> * If the inflated layout contains a {@link TextView} with an ID of * {@link android.R.id#text1} then that will be updated with the value given * to {@link #setText(CharSequence)}. Similarly, if this layout contains an * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with * the value given to {@link #setIcon(Drawable)}. * </p> * * @param layoutResId A layout resource to inflate and use as a custom tab view * @return The current instance for call chaining */ @NonNull public Tab setCustomView(@LayoutRes int layoutResId) { return setCustomView(LayoutInflater.from(mParent.getContext()).inflate(layoutResId, null)); } /** * Return the icon associated with this tab. * * @return The tab's icon */ @Nullable 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 */ @Nullable 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 */ @NonNull public Tab setIcon(@Nullable Drawable icon) { mIcon = icon; 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 text The text to display * @return The current instance for call chaining */ @NonNull public Tab setText(@Nullable 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 */ @NonNull public Tab setText(@StringRes 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); } /** * Returns true if this tab is currently selected. */ public boolean isSelected() { return mParent.getSelectedTabPosition() == mPosition; } /** * 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() */ @NonNull public Tab setContentDescription(@StringRes 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() */ @NonNull public Tab setContentDescription(@Nullable 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) */ @Nullable public CharSequence getContentDescription() { return mContentDesc; } } class TabView extends LinearLayout implements OnLongClickListener { private final Tab mTab; private TextView mTextView; private ImageView mIconView; private View mCustomView; private TextView mCustomTextView; private ImageView mCustomIconView; public TabView(Context context, Tab tab) { super(context); mTab = tab; 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()); } 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, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } mCustomView = custom; if (mTextView != null) { mTextView.setVisibility(GONE); } if (mIconView != null) { mIconView.setVisibility(GONE); mIconView.setImageDrawable(null); } mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); } else { // We do not have a custom view. Remove one if it already exists if (mCustomView != null) { removeView(mCustomView); mCustomView = null; } mCustomTextView = null; mCustomIconView = null; } if (mCustomView == null) { // If there isn't a custom view, we'll us our own in-built layouts if (mIconView == null) { ImageView iconView = getDefaultImageView(); addView(iconView, 0); mIconView = iconView; } if (mTextView == null) { TextView textView = getDefaultTextView(); addView(textView); mTextView = textView; } if (mTabTextColors != null) { mTextView.setTextColor(mTabTextColors); } updateTextAndIcon(tab, mTextView, mIconView); } else { // Else, we'll see if there is a TextView or ImageView present and update them if (mCustomTextView != null || mCustomIconView != null) { updateTextAndIcon(tab, mCustomTextView, mCustomIconView); } } } private void updateTextAndIcon(Tab tab, TextView textView, ImageView iconView) { final Drawable icon = tab.getIcon(); final CharSequence text = tab.getText(); if (iconView != null) { if (icon != null) { iconView.setImageDrawable(icon); iconView.setVisibility(VISIBLE); setVisibility(VISIBLE); } else { iconView.setVisibility(GONE); iconView.setImageDrawable(null); } iconView.setContentDescription(tab.getContentDescription()); } final boolean hasText = !TextUtils.isEmpty(text); if (textView != null) { if (hasText) { textView.setText(text); textView.setContentDescription(tab.getContentDescription()); textView.setVisibility(VISIBLE); setVisibility(VISIBLE); } else { textView.setVisibility(GONE); textView.setText(null); } } 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; } 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) { if (mSelectedIndicatorPaint.getColor() != color) { mSelectedIndicatorPaint.setColor(color); ViewCompat.postInvalidateOnAnimation(this); } } void setSelectedIndicatorHeight(int height) { if (mSelectedIndicatorHeight != height) { mSelectedIndicatorHeight = height; ViewCompat.postInvalidateOnAnimation(this); } } boolean childrenNeedLayout() { for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); if (child.getWidth() <= 0) { return true; } } return false; } void setIndicatorPositionFromTabPosition(int position, float positionOffset) { mSelectedPosition = position; mSelectionOffset = positionOffset; updateIndicatorPosition(); } float getIndicatorPosition() { return mSelectedPosition + mSelectionOffset; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); // If we've been layed out, 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) { ValueAnimator animator = mIndicatorAnimator = new ValueAnimator(); animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); animator.setDuration(duration); animator.setFloatValues(0, 1); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { final float fraction = animator.getAnimatedFraction(); setIndicatorPosition(AnimationUtils.lerp(startLeft, targetLeft, fraction), AnimationUtils.lerp(startRight, targetRight, fraction)); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animator) { mSelectedPosition = position; mSelectionOffset = 0f; } @Override public void onAnimationCancel(Animator animator) { mSelectedPosition = position; mSelectionOffset = 0f; } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start(); } } @Override public void draw(Canvas canvas) { super.draw(canvas); // Thick colored underline below the current selection if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, mIndicatorRight, getHeight(), mSelectedIndicatorPaint); } } } private static 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); } /** * A {@link ViewPager.OnPageChangeListener} class which contains the * necessary calls back to the provided {@link FlexibleTabLayout} so that the tab position is * kept in sync. * <p/> * <p>This class stores the provided FlexibleTabLayout weakly, meaning that you can use * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener) * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and * not cause a leak. */ public static class FlexibleTabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { private final WeakReference<FlexibleTabLayout> mFlexibleTabLayoutRef; private int mPendingSelection = -1; private int mScrollState; public FlexibleTabLayoutOnPageChangeListener(FlexibleTabLayout FlexibleTabLayout) { mFlexibleTabLayoutRef = new WeakReference<>(FlexibleTabLayout); } @Override public void onPageScrollStateChanged(int state) { mScrollState = state; } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { // This call is made before onPageScrolled() which can lead to a jerk if we just // selected the tab now. So we'll keep the position, and set it when we're idle again mPendingSelection = position; final FlexibleTabLayout flexibleTabLayout = mFlexibleTabLayoutRef.get(); if (flexibleTabLayout != null) { Tab tab = flexibleTabLayout.getTabAt(position); flexibleTabLayout.selectTab(tab); } } } /** * A {@link FlexibleTabLayout.OnTabSelectedListener} class which contains the necessary calls back * to the provided {@link ViewPager} so that the tab position is kept in sync. */ public static class ViewPagerOnTabSelectedListener implements FlexibleTabLayout.OnTabSelectedListener { private final ViewPager mViewPager; public ViewPagerOnTabSelectedListener(ViewPager viewPager) { mViewPager = viewPager; } @Override public void onTabSelected(FlexibleTabLayout.Tab tab) { mViewPager.setCurrentItem(tab.getPosition()); } @Override public void onTabUnselected(FlexibleTabLayout.Tab tab) { // No-op } @Override public void onTabReselected(FlexibleTabLayout.Tab tab) { // No-op } } private ImageView getDefaultImageView() { ImageView imageView = new ImageView(getContext()); float density = getResources().getDisplayMetrics().density; LayoutParams layoutParams = new LayoutParams((int) (24 * density), (int) (24 * density)); imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); imageView.setLayoutParams(layoutParams); return imageView; } private TextView getDefaultTextView() { TextView textView = new TextView(getContext()); textView.setMaxLines(2); textView.setEllipsize(TextUtils.TruncateAt.END); textView.setGravity(Gravity.CENTER); LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); textView.setLayoutParams(layoutParams); return textView; } }