package carbon.widget; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.LinearLayout; import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.StyleRes; import androidx.core.view.ViewCompat; import androidx.viewpager.widget.PagerAdapter; import com.annimon.stream.Stream; import carbon.R; import carbon.component.Component; import carbon.component.LayoutComponent; import carbon.databinding.CarbonTablayoutTabBinding; import carbon.recycler.RowFactory; public class TabLayout extends HorizontalScrollView { public static class Item { private CharSequence title; public Item(CharSequence title) { this.title = title; } public CharSequence getTitle() { return title; } } private static class ItemComponent extends LayoutComponent<Item> { ItemComponent(ViewGroup parent) { super(parent, R.layout.carbon_tablayout_tab); } private CarbonTablayoutTabBinding binding = CarbonTablayoutTabBinding.bind(getView()); @Override public void bind(Item data) { binding.carbonTabText.setText(data.title); } } ViewPager viewPager; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); LinearLayout content; private float indicatorPos = 0; private int selectedPage = 0; private float indicatorPos2 = 0; float indicatorHeight; DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(); boolean fixed = false; Item[] items; private RowFactory<Item> itemFactory; private ValueAnimator animator, animator2; private ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { position = Math.round(position + positionOffset); if (position != selectedPage) { View view = content.getChildAt(position); if (view == null) return; // TODO: what's really going on here? #130 if (animator != null) animator.cancel(); if (animator2 != null) animator2.cancel(); animator = ValueAnimator.ofFloat(indicatorPos, view.getLeft()); animator.setDuration(200); if (position > selectedPage) animator.setStartDelay(100); animator.setInterpolator(decelerateInterpolator); animator.addUpdateListener(animation -> { indicatorPos = (float) animation.getAnimatedValue(); postInvalidate(); }); animator.start(); animator2 = ValueAnimator.ofFloat(indicatorPos2, view.getRight()); animator2.setDuration(200); if (position < selectedPage) animator2.setStartDelay(100); animator2.setInterpolator(decelerateInterpolator); animator2.addUpdateListener(animation -> { indicatorPos2 = (float) animation.getAnimatedValue(); postInvalidate(); }); animator2.start(); setSelectedPage(position); if (content.getChildAt(selectedPage).getLeft() - getScrollX() < 0) { smoothScrollTo(content.getChildAt(selectedPage).getLeft(), 0); } else if (content.getChildAt(selectedPage).getRight() - getScrollX() > getWidth()) { smoothScrollTo(content.getChildAt(selectedPage).getRight() - getWidth() + getPaddingLeft(), 0); } } } @Override public void onPageSelected(int position) { } @Override public void onPageScrollStateChanged(int state) { } }; public TabLayout(Context context) { super(context, null, R.attr.carbon_tabLayoutStyle); initPagerTabStrip(null, R.attr.carbon_tabLayoutStyle, R.style.carbon_TabLayout); } public TabLayout(Context context, AttributeSet attrs) { super(context, attrs, R.attr.carbon_tabLayoutStyle); initPagerTabStrip(attrs, R.attr.carbon_tabLayoutStyle, R.style.carbon_TabLayout); } public TabLayout(Context context, AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); initPagerTabStrip(attrs, defStyleAttr, R.style.carbon_TabLayout); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public TabLayout(Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initPagerTabStrip(attrs, defStyleAttr, defStyleRes); } private void initPagerTabStrip(AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { int layoutDirection = ViewCompat.getLayoutDirection(this); ViewCompat.setLayoutDirection(this, ViewCompat.LAYOUT_DIRECTION_LTR); content = new LinearLayout(getContext()); ViewCompat.setLayoutDirection(content, layoutDirection); addView(content, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.TabLayout, defStyleAttr, defStyleRes); setIndicatorHeight(a.getDimension(R.styleable.TabLayout_carbon_indicatorWidth, 2)); setFixed(a.getBoolean(R.styleable.TabLayout_carbon_fixedTabs, true)); itemFactory = ItemComponent::new; a.recycle(); setHorizontalFadingEdgeEnabled(false); setHorizontalScrollBarEnabled(false); initTabs(); } public void setViewPager(final ViewPager viewPager) { if (viewPager != null) viewPager.removeOnPageChangeListener(pageChangeListener); this.viewPager = viewPager; if (viewPager != null) { viewPager.addOnPageChangeListener(pageChangeListener); PagerAdapter adapter = viewPager.getAdapter(); if (adapter != null && items == null) items = Stream.range(0, adapter.getCount()).map(it -> new Item(adapter.getPageTitle(it))).toArray(Item[]::new); } initTabs(); } @Deprecated public void setItemLayout(int itemLayoutId) { } public void setItemFactory(RowFactory<Item> factory) { this.itemFactory = factory; initTabs(); } public void setItems(Item[] items) { this.items = items; initTabs(); } private void initTabs() { content.removeAllViews(); if (items == null) return; for (int i = 0; i < items.length; i++) { Component<Item> component = itemFactory.create(this); component.setData(items[i]); content.addView(component.getView(), new LinearLayout.LayoutParams(fixed ? 0 : ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); component.getView().setSelected(i == 0); final int finalI = i; component.getView().setOnClickListener(__ -> { if (viewPager != null) { viewPager.setCurrentItem(finalI); } else { pageChangeListener.onPageScrolled(finalI, 0, 0); } }); } } @Override public void draw(@NonNull Canvas canvas) { super.draw(canvas); if (content.getChildCount() == 0) return; if (indicatorPos == indicatorPos2) indicatorPos2 = content.getChildAt(selectedPage).getWidth(); paint.setColor(getTint().getColorForState(getDrawableState(), getTint().getDefaultColor())); canvas.drawRect(indicatorPos + getPaddingLeft(), getHeight() - indicatorHeight - getPaddingBottom(), indicatorPos2 + getPaddingLeft(), getHeight(), paint); } public ViewPager getViewPager() { return viewPager; } public boolean isFixed() { return fixed; } public void setFixed(boolean fixed) { this.fixed = fixed; setFillViewport(fixed); initTabs(); } public float getIndicatorHeight() { return indicatorHeight; } public void setIndicatorHeight(float indicatorHeight) { this.indicatorHeight = indicatorHeight; postInvalidate(); } public void setSelectedPage(int position) { if (content.getChildCount() > selectedPage) content.getChildAt(selectedPage).setSelected(false); selectedPage = position; if (content.getChildCount() > selectedPage) content.getChildAt(selectedPage).setSelected(true); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); return new SavedState(superState, selectedPage, getScrollX(), indicatorPos, indicatorPos2); } @Override protected void onRestoreInstanceState(Parcelable state) { final SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); setSelectedPage(savedState.getSelectedPage()); indicatorPos = savedState.getIndicatorPos(); indicatorPos2 = savedState.getIndicatorPos2(); post(new Runnable() { public void run() { setScrollX(savedState.getScroll()); } }); } @Override protected void dispatchSaveInstanceState(@NonNull SparseArray<Parcelable> container) { super.dispatchFreezeSelfOnly(container); } @Override protected void dispatchRestoreInstanceState(@NonNull SparseArray<Parcelable> container) { super.dispatchThawSelfOnly(container); } protected static class SavedState extends BaseSavedState { private final int selectedPage; private final int scroll; private final float indicatorPos; private final float indicatorPos2; private SavedState(Parcelable superState, int selectedPage, int scrollX, float indicatorPos, float indicatorPos2) { super(superState); this.selectedPage = selectedPage; this.scroll = scrollX; this.indicatorPos = indicatorPos; this.indicatorPos2 = indicatorPos2; } private SavedState(Parcel in) { super(in); selectedPage = in.readInt(); scroll = in.readInt(); indicatorPos = in.readFloat(); indicatorPos2 = in.readFloat(); } public int getSelectedPage() { return selectedPage; } public int getScroll() { return scroll; } public float getIndicatorPos() { return indicatorPos; } public float getIndicatorPos2() { return indicatorPos2; } @Override public void writeToParcel(@NonNull Parcel destination, int flags) { super.writeToParcel(destination, flags); destination.writeInt(selectedPage); destination.writeInt(scroll); destination.writeFloat(indicatorPos); destination.writeFloat(indicatorPos2); } public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }