package com.material.widget; import android.animation.Animator; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.*; import android.os.Build; import android.os.Handler; import android.support.annotation.NonNull; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.util.AttributeSet; import android.util.TypedValue; import android.view.ActionMode; import android.view.Gravity; import android.view.MotionEvent; import android.view.ViewTreeObserver; import android.widget.*; import java.util.ArrayList; import java.util.List; /** * Created by IntelliJ IDEA. * User: keith. * Date: 14-9-26. * Time: 13:39. * Thanks for https://github.com/astuetz/PagerSlidingTabStrip */ public class TabIndicator extends HorizontalScrollView implements Animator.AnimatorListener { private static final String TAG = TabIndicator.class.getSimpleName(); private static final long ANIMATION_DURATION = 150; private static final int StateNormal = 1; private static final int StateTouchDown = 2; private static final int StateTouchUp = 3; private final PageListener mPageListener = new PageListener(); private int mMaxColumn; private int mTextSize; private int mTextColor; private int mTextSelectedColor; private int mTextDisabledColor; private int mRippleColor; private int mUnderLineColor; private int mUnderLineHeight; private int mNavButtonWidth; private int mCurrentIndex; private OnPageChangeListener mOnPageChangeListener; private TabContainer tabsContainer; private ViewPager pager; private TabView mCurrentTab; private NavButton mCurrentNavButton; private int[] firstIndexSet = new int[]{}; private int tabCount; private Rect lineRect = new Rect(); private List<NavButton> mNavButtonList = new ArrayList<NavButton>(); private List<TabView> mTabList = new ArrayList<TabView>(); private Paint linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint ripplePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private ActionMode mActionMode; public TabIndicator(Context context) { this(context, null); } public TabIndicator(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TabIndicator(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setFillViewport(true); setHorizontalScrollBarEnabled(false); tabsContainer = new TabContainer(context); tabsContainer.setOrientation(LinearLayout.HORIZONTAL); tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); addView(tabsContainer); TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.TabIndicator); mTextSize = attributes.getDimensionPixelSize(R.styleable.TabIndicator_tab_text_size, getResources().getDimensionPixelSize(R.dimen.tab_text_size)); mTextColor = attributes.getColor(R.styleable.TabIndicator_tab_text_color, getResources().getColor(R.color.tab_text_color)); mTextSelectedColor = attributes.getColor(R.styleable.TabIndicator_tab_text_selected_color, getResources().getColor(R.color.tab_text_selected_color)); mTextDisabledColor = attributes.getColor(R.styleable.TabIndicator_tab_text_disabled_color, getResources().getColor(R.color.tab_text_disabled_color)); mRippleColor = attributes.getColor(R.styleable.TabIndicator_tab_ripple_color, getResources().getColor(R.color.tab_ripple_color)); mUnderLineColor = attributes.getColor(R.styleable.TabIndicator_tab_underline_color, getResources().getColor(R.color.tab_underline_color)); mUnderLineHeight = attributes.getDimensionPixelSize(R.styleable.TabIndicator_tab_underline_height, getResources().getDimensionPixelSize(R.dimen.tab_underline_height)); mMaxColumn = attributes.getInteger(R.styleable.TabIndicator_tab_max_column, getResources().getInteger(R.integer.tab_max_column)); mNavButtonWidth = getResources().getDimensionPixelSize(R.dimen.tab_nav_button_width); attributes.recycle(); linePaint.setStyle(Paint.Style.FILL); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (pager != null) { notifyDataSetChanged(); final int mTmpIndex = pager.getCurrentItem(); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { TabIndicator.this.animatedSelectCurrentTab(mTmpIndex); } }, 100); } } // TODO programmatically change max column public void setMaxColumn(int column) { this.mMaxColumn = column; } // TODO programmatically set current tab index public void setCurrentIndex(int index) { } // TODO programmatically set underline height public void setUnderLineHeight(int pixel) { this.mUnderLineHeight = pixel; } public void setTextColor(int color) { this.mTextColor = color; invalidate(); for (TabView tabView : mTabList) { tabView.setTextColor(mTextColor); } setTextSelectedColor(mTextSelectedColor); invalidate(); } public void setTextSelectedColor(int color) { this.mTextSelectedColor = color; if (mCurrentTab != null) { mCurrentTab.setTextColor(mTextSelectedColor); } invalidate(); } // TODO disable tab indicator public void setTextDisabledColor(int color) { this.mTextDisabledColor = color; } public void setRippleColor(int color) { this.mRippleColor = color; invalidate(); } public void setUnderLineColor(int color) { this.mUnderLineColor = color; invalidate(); } public void setOnPageChangeListener(OnPageChangeListener listener) { this.mOnPageChangeListener = listener; } public void setViewPager(ViewPager pager) { this.pager = pager; if (pager.getAdapter() == null) { throw new IllegalStateException("ViewPager does not have adapter instance."); } pager.setOnPageChangeListener(mPageListener); notifyDataSetChanged(); } public void notifyDataSetChanged() { mCurrentIndex = 0; tabsContainer.removeAllViews(); mNavButtonList = new ArrayList<NavButton>(); mTabList = new ArrayList<TabView>(); mCurrentIndex = 0; if (pager.getAdapter() instanceof TabTextProvider) { layoutTabItem(); } } private void layoutTabItem() { tabCount = pager.getAdapter().getCount(); getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { getViewTreeObserver().removeGlobalOnLayoutListener(this); } else { getViewTreeObserver().removeOnGlobalLayoutListener(this); } TabTextProvider provider = (TabTextProvider) pager.getAdapter(); int itemWidth = getMeasuredWidth() / mMaxColumn; if (tabCount <= mMaxColumn) { firstIndexSet = new int[]{0}; if (tabCount < mMaxColumn) { itemWidth = Math.round(getMeasuredWidth() / tabCount); } for (int i = 0; i < tabCount; i++) { addTab(i, itemWidth, provider.getText(i)); } } else { int remain = tabCount % mMaxColumn; int segment = (remain == 0 ? 0 : 1) + (tabCount - remain) / mMaxColumn; List<Integer> tempSet = new ArrayList<Integer>(); for (int m = 0; m < segment; m++) { tempSet.add(m * mMaxColumn); if (m == 0) { itemWidth = (getMeasuredWidth() - mNavButtonWidth) / mMaxColumn; for (int n = 0; n < mMaxColumn; n++) { addTab(n, itemWidth, provider.getText(n)); if (n == mMaxColumn - 1) { addNavButton(n, NavButton.FORWARD); } } } else if (m == segment - 1) { int size = remain == 0 ? mMaxColumn : remain; itemWidth = (getMeasuredWidth() - mNavButtonWidth) / size; for (int n = 0; n < remain; n++) { int index = m * mMaxColumn + n; if (n == 0) { addNavButton(index, NavButton.BACKWARD); } addTab(index, itemWidth, provider.getText(index)); } } else { itemWidth = (getMeasuredWidth() - mNavButtonWidth * 2) / mMaxColumn; for (int n = 0; n < mMaxColumn; n++) { int index = m * mMaxColumn + n; if (n == 0) { addNavButton(index, NavButton.BACKWARD); } addTab(index, itemWidth, provider.getText(index)); if (n == mMaxColumn - 1) { addNavButton(index, NavButton.FORWARD); } } } } firstIndexSet = new int[tempSet.size()]; for (int i = 0; i < tempSet.size(); ++i) { firstIndexSet[i] = tempSet.get(i); } } } }); } private void addTab(int index, int width, String title) { TabView tabView = new TabView(getContext()); tabView.setIndex(index); tabView.setText(title); tabView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); tabView.setTextColor(mTextColor); tabView.setWidth(width); tabView.setTabWidth(width); tabView.setOnSelectTabListener(new OnSelectTabListener() { @Override public void onSelect(TabView tabView) { mCurrentIndex = tabView.getIndex(); mCurrentTab = tabView; tabsContainer.postInvalidate(); pager.setCurrentItem(mCurrentIndex); } }); // Default select the first tab if (index == 0) { mCurrentTab = tabView; } mTabList.add(tabView); tabsContainer.addView(tabView, new LinearLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.MATCH_PARENT)); } private void addNavButton(int index, int direction) { NavButton navButton = new NavButton(getContext()); navButton.setIndex(index); navButton.setType(direction); navButton.setMaxWidth(mNavButtonWidth); navButton.setMinimumWidth(mNavButtonWidth); navButton.setOnNavListener(new OnNavListener() { @Override public void onNav(NavButton button) { mCurrentNavButton = button; int count; int distance = getMeasuredWidth(); switch (button.getType()) { case NavButton.FORWARD: { count = (mCurrentNavButton.getIndex() + 1) / mMaxColumn; ObjectAnimator objectAnimator = ObjectAnimator.ofInt(TabIndicator.this, "scrollX", distance * count); objectAnimator.addListener(TabIndicator.this); objectAnimator.setDuration(ANIMATION_DURATION); objectAnimator.start(); } break; case NavButton.BACKWARD: { count = mCurrentNavButton.getIndex() / mMaxColumn - 1; ObjectAnimator objectAnimator = ObjectAnimator.ofInt(TabIndicator.this, "scrollX", distance * count); objectAnimator.addListener(TabIndicator.this); objectAnimator.setDuration(ANIMATION_DURATION); objectAnimator.start(); } break; } } }); mNavButtonList.add(navButton); tabsContainer.addView(navButton, new LinearLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.MATCH_PARENT)); } private void animatedSelectCurrentTab(int index) { if (index != mCurrentIndex) { if (mCurrentIndex % mMaxColumn == 0 && index < mCurrentIndex) { for (NavButton button : mNavButtonList) { if (button.getIndex() == mCurrentIndex) { button.performNavAction(); break; } } } else if (index % mMaxColumn == 0 && index > mCurrentIndex) { for (NavButton button : mNavButtonList) { if (button.getIndex() == mCurrentIndex) { button.performNavAction(); break; } } } else { for (TabView tabView : mTabList) { if (tabView.getIndex() == index) { tabView.performSelectAction(); break; } } } } } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { return false; } @Override public boolean onInterceptTouchEvent(@NonNull MotionEvent event) { return false; } @Override public void onAnimationStart(Animator animator) { if (mCurrentNavButton != null) { switch (mCurrentNavButton.getType()) { case NavButton.FORWARD: { mCurrentIndex = mCurrentNavButton.getIndex() + 1; if (mCurrentIndex < mTabList.size()) { mCurrentTab = mTabList.get(mCurrentIndex); tabsContainer.postInvalidate(); } } break; case NavButton.BACKWARD: { mCurrentIndex = mCurrentNavButton.getIndex() - 1; if (mCurrentIndex < mTabList.size()) { mCurrentTab = mTabList.get(mCurrentIndex); tabsContainer.postInvalidate(); } } break; } } } @Override public void onAnimationEnd(Animator animator) { if (mCurrentNavButton != null) { switch (mCurrentNavButton.getType()) { case NavButton.FORWARD: pager.setCurrentItem(mCurrentIndex); break; case NavButton.BACKWARD: pager.setCurrentItem(mCurrentIndex); break; } } } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } public interface OnSelectTabListener { void onSelect(TabView tabView); } public interface OnNavListener { void onNav(NavButton button); } public interface TabTextProvider { public String getText(int position); } // ================================================== Tab container =========================================== // private class TabContainer extends LinearLayout { private long mStartTime; public TabContainer(Context context) { this(context, null); } public TabContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TabContainer(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setWillNotDraw(false); } // TODO animate underline to selected tab public void animateToSelectedTab(int lastIndex, int currentIndex) { } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); linePaint.setColor(mUnderLineColor); // Animate translation underline if (mCurrentTab != null) { // Clear text select status for (TabView tabView : mTabList) { tabView.setTextColor(mTextColor); } mCurrentTab.setTextColor(mTextSelectedColor); int x; if (mCurrentTab.getIndex() < mMaxColumn) { x = mCurrentTab.getIndex() * mCurrentTab.getTabWidth(); lineRect.left = x; lineRect.top = getHeight() - mUnderLineHeight; lineRect.right = x + mCurrentTab.getWidth(); lineRect.bottom = getHeight(); } else { int remain = mCurrentTab.getIndex() % mMaxColumn; int segment = (mCurrentTab.getIndex() - remain) / mMaxColumn; x = mNavButtonWidth + segment * TabIndicator.this.getMeasuredWidth() + remain * mCurrentTab.getTabWidth(); lineRect.left = x; lineRect.top = getHeight() - mUnderLineHeight; lineRect.right = x + mCurrentTab.getWidth(); lineRect.bottom = getHeight(); } canvas.drawRect(lineRect, linePaint); } } } // ================================================== Tab item view =========================================== // private class TabView extends Button { private static final int StateRippleNormal = 0; private static final int StateRippleTriggerStart = 1; private static final int StateRippleTriggerEnd = 2; private static final int StateRippleProliferationStart = 3; private static final int StateRippleProliferationEnd = 4; private int mRippleState = StateRippleNormal; private int mState = StateNormal; private int index; private int mTabWidth; private int mEndRadius; private long mStartTime; private Rect mFingerRect; private boolean mMoveOutside; private Point mTouchPoint = new Point(); private OnSelectTabListener mOnSelectTabListener; public TabView(Context context) { this(context, null); } public TabView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TabView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { setBackgroundDrawable(null); } else { setBackground(null); } setGravity(Gravity.CENTER); setBackgroundColor(Color.TRANSPARENT); } public void performSelectAction() { if (mOnSelectTabListener != null) { mOnSelectTabListener.onSelect(TabView.this); } } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mMoveOutside = false; mFingerRect = new Rect(getLeft(), getTop(), getRight(), getBottom()); mTouchPoint.set(Math.round(event.getX()), Math.round(event.getY())); mState = StateTouchDown; mRippleState = StateRippleTriggerStart; mStartTime = System.currentTimeMillis(); invalidate(); break; case MotionEvent.ACTION_MOVE: if (!mFingerRect.contains(getLeft() + (int) event.getX(), getTop() + (int) event.getY())) { mMoveOutside = true; mState = StateNormal; mRippleState = StateRippleNormal; mStartTime = System.currentTimeMillis(); invalidate(); } break; case MotionEvent.ACTION_UP: if (!mMoveOutside) { performSelectAction(); mState = StateTouchUp; if (mRippleState == StateRippleTriggerEnd) { mRippleState = StateRippleProliferationStart; mStartTime = System.currentTimeMillis(); invalidate(); } } break; } return true; } @Override protected void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); ripplePaint.setColor(mRippleColor); int radius = 0; long elapsed = System.currentTimeMillis() - mStartTime; switch (mRippleState) { case StateRippleTriggerStart: { ripplePaint.setAlpha(120); if (elapsed < ANIMATION_DURATION) { radius = Math.round(elapsed * getWidth() / 2 / ANIMATION_DURATION); postInvalidate(); } else { radius = getWidth() / 2; mRippleState = StateRippleTriggerEnd; } mEndRadius = radius; } break; case StateRippleProliferationStart: { if (elapsed < ANIMATION_DURATION) { int alpha = Math.round((ANIMATION_DURATION - elapsed) * 120 / ANIMATION_DURATION); ripplePaint.setAlpha(alpha); radius = mEndRadius + Math.round(elapsed * getWidth() / 2 / ANIMATION_DURATION); postInvalidate(); } else { ripplePaint.setAlpha(0); radius = 0; mState = StateNormal; postInvalidate(); mRippleState = StateRippleProliferationEnd; } } break; case StateRippleNormal: radius = 0; break; } canvas.drawCircle(mTouchPoint.x, mTouchPoint.y, radius, ripplePaint); switch (mRippleState) { case StateRippleTriggerEnd: if (mState == StateTouchUp) { mRippleState = StateRippleProliferationStart; mStartTime = System.currentTimeMillis(); invalidate(); } break; case StateRippleProliferationEnd: mState = StateNormal; mRippleState = StateRippleNormal; break; } } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public void setOnSelectTabListener(OnSelectTabListener listener) { this.mOnSelectTabListener = listener; } public int getTabWidth() { return mTabWidth; } public void setTabWidth(int width) { this.mTabWidth = width; } } // =============================================== Navigation button ========================================== // private class NavButton extends ImageButton { private static final int StateRippleNormal = 0; private static final int StateRippleTriggerStart = 1; private static final int StateRippleTriggerEnd = 2; private static final int StateRippleProliferationStart = 3; private static final int StateRippleProliferationEnd = 4; private int mRippleState = StateRippleNormal; public static final int FORWARD = 1; public static final int BACKWARD = 2; private int mState = StateNormal; private int index; private int mType; private int mEndRadius; private long mStartTime; private Rect mFingerRect; private boolean mMoveOutside; private OnNavListener mOnNavListener; public NavButton(Context context) { this(context, null); } public NavButton(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NavButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { setBackgroundDrawable(null); } else { setBackground(null); } setScaleType(ScaleType.CENTER); setBackgroundColor(Color.TRANSPARENT); } public void performNavAction() { if (mOnNavListener != null) { mOnNavListener.onNav(NavButton.this); } } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mMoveOutside = false; mFingerRect = new Rect(getLeft(), getTop(), getRight(), getBottom()); mState = StateTouchDown; mRippleState = StateRippleTriggerStart; mStartTime = System.currentTimeMillis(); invalidate(); break; case MotionEvent.ACTION_MOVE: if (!mFingerRect.contains(getLeft() + (int) event.getX(), getTop() + (int) event.getY())) { mMoveOutside = true; mState = StateNormal; mRippleState = StateRippleNormal; mStartTime = System.currentTimeMillis(); invalidate(); } break; case MotionEvent.ACTION_UP: if (!mMoveOutside) { performNavAction(); mState = StateTouchUp; if (mRippleState == StateRippleTriggerEnd) { mRippleState = StateRippleProliferationStart; mStartTime = System.currentTimeMillis(); invalidate(); } } break; } return true; } @Override protected void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); ripplePaint.setColor(mRippleColor); int radius = 0; long elapsed = System.currentTimeMillis() - mStartTime; switch (mRippleState) { case StateRippleTriggerStart: { ripplePaint.setAlpha(120); if (elapsed < ANIMATION_DURATION) { radius = Math.round(elapsed * getWidth() / 2 / ANIMATION_DURATION); postInvalidate(); } else { radius = getWidth() / 2; mRippleState = StateRippleTriggerEnd; } mEndRadius = radius; } break; case StateRippleProliferationStart: { if (elapsed < ANIMATION_DURATION) { int alpha = Math.round((ANIMATION_DURATION - elapsed) * 120 / ANIMATION_DURATION); ripplePaint.setAlpha(alpha); radius = mEndRadius + Math.round(elapsed * getWidth() / 2 / ANIMATION_DURATION); postInvalidate(); } else { ripplePaint.setAlpha(0); radius = 0; mState = StateNormal; postInvalidate(); mRippleState = StateRippleProliferationEnd; } } break; case StateRippleNormal: radius = 0; break; } canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, ripplePaint); switch (mRippleState) { case StateRippleTriggerEnd: if (mState == StateTouchUp) { mRippleState = StateRippleProliferationStart; mStartTime = System.currentTimeMillis(); invalidate(); } break; case StateRippleProliferationEnd: mState = StateNormal; mRippleState = StateRippleNormal; break; } } public void setOnNavListener(OnNavListener listener) { this.mOnNavListener = listener; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public int getType() { return mType; } public void setType(int type) { this.mType = type; switch (this.mType) { case FORWARD: setImageResource(R.drawable.ic_forward); break; case BACKWARD: setImageResource(R.drawable.ic_backward); break; } } } public void setActionMode(ActionMode mActionMode) { this.mActionMode = mActionMode; pager.setOnPageChangeListener(mPageListener); } private class PageListener implements OnPageChangeListener { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } // TODO Scroll indicator animated } @Override public void onPageSelected(int position) { if (mActionMode != null) mActionMode.finish(); TabIndicator.this.animatedSelectCurrentTab(position); if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(position); } } @Override public void onPageScrollStateChanged(int state) { if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrollStateChanged(state); } } } }