Slide belt

What are Android sliders, for example





image.png

The black bar is typically used with a viewpager or multiple fragments, hence the so-called Indicator. So why do I call it a sliding strip? As an example, there is a control called TabLayout, which is often used with viewPager. The inside of a TabLayout comes with this bar indicator.





image.png

Its official name is SlidingTabStrip, which I translate as sliding-strip, sliding-bar. Tab is the name associated with TabLayout, and I can call it SlidingStrip for the rest of my life

Two. Custom sliding belt

1. Why customize SlidingStrip

Why re-build the wheel when the controls are already wrapped for me? Sometimes a special situation does not apply to a TabLayout, and you need to customize a Tab or some other SlidingStrip state that does not work with a Tab. Then you have to write a SlidingStrip.

2. How to customize SlidingStrip

How to customize, of course, everyone has their own way, or you can imagine the way to achieve this function, but since the official has written here, I personally will follow the official practice. As for the official how to do, we can only look at the source, look at TabLayout inside the SlidingTabStrip class

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; private ValueAnimatorCompat mIndicatorAnimator; 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) { if (mIndicatorAnimator ! = null && mIndicatorAnimator.isRunning()) { mIndicatorAnimator.cancel(); } mSelectedPosition = position; mSelectionOffset = positionOffset; updateIndicatorPosition(); } float getIndicatorPosition() { return mSelectedPosition + mSelectionOffset; } @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { ...... } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { ...... } 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) { if (mIndicatorAnimator ! = null && mIndicatorAnimator.isRunning()) { mIndicatorAnimator.cancel(); } final boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; final View targetView = getChildAt(position); if (targetView == null) { // If we don't have a view, just update the position now and return updateIndicatorPosition();  return; } 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) { ValueAnimatorCompat animator = mIndicatorAnimator = ViewUtils.createAnimator(); animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); animator.setDuration(duration); animator.setFloatValues(0, 1); animator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimatorCompat animator) { final float fraction = animator.getAnimatedFraction(); setIndicatorPosition( AnimationUtils.lerp(startLeft, targetLeft, fraction), AnimationUtils.lerp(startRight, targetRight, fraction)); }}); animator.setListener(new ValueAnimatorCompat.AnimatorListenerAdapter() { @Override public void onAnimationEnd(ValueAnimatorCompat animator) { mSelectedPosition = position; mSelectionOffset = 0f; }}); 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); }}}Copy the code

PS: I temporarily hide the onMeasure and onLayout methods inside. (1) You can see from the draw method that the visual bar was drawn in Paint. So whenever switch TAB will be redrawn, so this method can be found in many places: ViewCompat. PostInvalidateOnAnimation (this); GetChildAt (); getChildAt (); getChildAt (); getChildAt (); getChildAt ()

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); }Copy the code

Gets the current subview, set the left dot to the left of the subview from the viewGroup, and set the right dot to the right of the subview from the viewGroup. I’m just going to post a coordinate diagram of view





image.png


PS: As a bonus, another method is to get the width of the view relative to the top left corner of the screen

int[] wandh = new int[2];
view.getLocationInWindow(wandh);
Copy the code

Wandh [0] is wide, wandh[1] is high

If (mSelectionOffset > 0f && mSelectedPosition < getChildCount() -1) So mSelectionOffset represents the viewPager offset, and it only goes into the judgment when the viewPager slides, so I don’t have to worry about that. After setting the width and height, finally re-paint the cloth

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); }}Copy the code

This is the whole process of the slider display. For the code, there may be some places and some algorithms that are not easy to understand, and I don’t understand them all, but the flow is easy to see: Draw the rectangle -> call updateIndicatorPosition () if there is a switch -> call setIndicatorPosition () to redraw.

When do you call updateIndicatorPosition () to set the left and right points and redraw them





image.png

And then say where does setScrollPosition appear





image.png


This is the slide listener for the viewPager, and you can also see the positionOffset passed in here





image.png





image.png

SelectTab is a TAB click-toggle event, and you can see that the offset passed here is 0.

This way we can see the whole process of listening for a swipe or click, changing the left and right points and then redrawing the onDraw call

The rest of the source CODE I will not speak, after all, I am not all understand, this force can not be installed.

Three.TabLayout basic principle

Now that we know the principle, we can write our own SlidingStrip. Although SlidingStrip implementation principle is the same as tabLayout, but a little different.

The SlidingTabStrip in tabLayout contains tabs.





image.png





image.png





image.png

TabLayout is a HorizontalScrollView, and its child view is a SlidingStrip, which contains a TabView.

Why I want to say this, because I met a problem before, I used to want to get the Tablayout tabView (child view), I could not find a way to get it, the Internet to find a lot of articles are very bullshit, until I read the source code, I did not know that you can get tabView (child view).

((LinearLayout)mTabLayout.getChildAt(0)).getChildAt(i)
Copy the code

Four. My slide tape

After so much, we finally get to the point. Let’s write a simple sliding tape by ourselves, and we will have time to improve it slowly in the future. Ps: My approach is different from that of tablayout. I don’t want to add a TabView to the SlidingStrip. I want to separate the TabView from the SlidingStrip, and the SlidingStrip only does the logic inside the SlidingStrip.

1.SlidingStrip
public class NewLineIndicator extends View{ private ViewGroup viewgroup; private List<View> chindViewList = new ArrayList<>(); Private int position = 0; Private float mLeft = 0; private float mLeft = 0; private float mRight = 0; private Paint paint; private NewLineIndicatorAdapter adapter; public NewLineIndicator(Context context) { super(context); } public NewLineIndicator(Context context, AttributeSet attrs) { super(context, attrs); } public NewLineIndicator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private void initChildView(){ paint = new Paint(); paint.setColor(getResources().getColor(R.color.price_color)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mLeft = viewgroup.getLeft() + chindViewList.get(position).getLeft(); mRight = viewgroup.getLeft() + chindViewList.get(position).getRight(); Log.v("wori","mLeft"+chindViewList.get(position).getLeft()+" mRight"+chindViewList.get(position).getRight()); canvas.drawRect(mLeft, 0, mRight, getHeight(), paint); } public void setPosition(int position) { this.position = position; } public void setAdapter(NewLineIndicatorAdapter adapter) { this.adapter = adapter; viewgroup = adapter.getTabLayout(); chindViewList = adapter.getChildViewList(); initChildView(); adapter.setIndicator(this); adapter.initIndicator(); } /** * public void tabChange(){// invalidate(); ViewCompat.postInvalidateOnAnimation(this); }}Copy the code

I’m inheriting a View, and I’m running out of time, but I think I should inherit a ViewGroup, and I’ll change that when I have time, but remember, although you can do this as a view, it should be a viewgroup, not a view, so if you write an inherited Layout or something like that, As long as it’s a viewgroup.

2. The adapter
public abstract class NewLineIndicatorAdapter { private Context context; private TabLayout mTabLayout; private NewLineIndicator mIndicator; public NewLineIndicatorAdapter(Context context,TabLayout mTabLayout){ this.context = context; this.mTabLayout = mTabLayout; } public void setIndicator(NewLineIndicator mIndicator) { this.mIndicator = mIndicator; } public void initIndicator(){ setTabLayoutChange(); } / * * * which TAB Settings TabLayout click listen * / private void setTabLayoutChange () {mTabLayout. SetOnTabSelectedListener (new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { mIndicator.setPosition(tab.getPosition()); mIndicator.tabChange(); } @Override public void onTabUnselected(TabLayout.Tab tab) { } @Override public void onTabReselected(TabLayout.Tab tab) {}}); } public TabLayout getTabLayout() { return mTabLayout; } public List<View> getChildViewList(){ List<View> childViewList = new ArrayList<>(); for (int i = 0; i < ((LinearLayout)mTabLayout.getChildAt(0)).getChildCount(); i++) { childViewList.add(((LinearLayout)mTabLayout.getChildAt(0)).getChildAt(i)); } return childViewList; }}Copy the code

In order to compare with the native TabLayout later, I used the TabLayout TAB for the adapter.

3. Call

(1) The adapter is an abstract method, inherited first

public static class TestIndicatorAdapter extends NewLineIndicatorAdapter{ public TestIndicatorAdapter(Context context, TabLayout mTabLayout) { super(context, mTabLayout); }}Copy the code

(2) call

 adapter = new TestIndicatorAdapter(this,tab);
indicator.setAdapter(adapter);
Copy the code

Code is very simple, I think there is no need to explain or source code to repeat, but I am writing a small demo, so did not write complete, interface what I did not define, directly with abstract class, in a hurry.

4. Effect display




15083827033281508382690972.gif

Maybe I can’t see it very clearly. The red bar below is my custom, and the green bar above is tablayout’s own.


I’m technically done with this feature, but I haven’t perfected it.

Some friends will say that they have a sliding effect, your flash effect is too lowB, I can only say that there is no way, the original animation, I am at a loss, ANIMATION I dare not install 13

private ValueAnimatorCompat mIndicatorAnimator;
Copy the code

In fact, if you use Tablayout as a TAB, tabMode=”scrollable” will cause problems. Serious bugs like this can happen.





15083836572711508383651840.gif

When you slide to the rear, it doesn’t match, and one screen can hold 5. When you slide to the sixth, the slide strip will disappear, because the left point of the child view after the sixth goes beyond the screen, so it doesn’t disappear, but slide out of the screen.

How to solve this problem, in fact, don’t let the TAB slide on the line, just kidding, how can solve the problem so arbitrary, let’s see how to solve the native.

If a tabLayout does not listen for the HorizontalScrollView, it should be in the important refresh method.

private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
            boolean updateIndicatorPosition) {
        final int roundedPosition = Math.round(position + positionOffset);
        if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
            return;
        }

        // Set the indicator position, if enabled
        if (updateIndicatorPosition) {
            mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
        }

        // Now update the scroll position, canceling any running animation
        if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
            mScrollAnimator.cancel();
        }
        scrollTo(calculateScrollXForTab(position, positionOffset), 0);

        // Update the 'selected state' view as we scroll, if enabled
        if (updateSelectedText) {
            setSelectedTabView(roundedPosition);
        }
    }
Copy the code

The first piece of code is rounding, the second piece of code is redrawing, and the third piece of code is stopping the animation. The key is definitely in the next few lines.

        scrollTo(calculateScrollXForTab(position, positionOffset), 0);

        // Update the 'selected state' view as we scroll, if enabled
        if (updateSelectedText) {
            setSelectedTabView(roundedPosition);
        }
Copy the code

Take a look at the first line to which horizontal it scrolls the TabLayout

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 selectedChild.getLeft() + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f)) + (selectedChild.getWidth() / 2) - (getWidth() / 2); } return 0; }Copy the code

Ps: In this case we do not consider the positionOffset by default

(1) mMode is what we set the scrollable to be 0, so we go into if statement (2) and we get the width of the child view that we clicked on and the width of the next child view (3) and then we get back, I don’t know how we got this formula, but we just move the child view that we clicked on to the middle.

Move to the middle and call setSelectedTabView

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); }}}Copy the code

I’m going to be confused by this, this child.setselected (I == position); I don’t get it. It’s like this is just changing the state, nothing to do with redrawing.


I looked at the code and realized that I was looking in the wrong place. After CLICKING TAB, I called the method animateToTab





image.png

private void animateToTab(int newPosition) { if (newPosition == Tab.INVALID_POSITION) { return; } if (getWindowToken() == null || ! ViewCompat.isLaidOut(this) || mTabStrip.childrenNeedLayout()) { // 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); if (startScrollX ! = targetScrollX) { if (mScrollAnimator == null) { mScrollAnimator = ViewUtils.createAnimator(); mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); mScrollAnimator.setDuration(ANIMATION_DURATION); mScrollAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimatorCompat animator) { scrollTo(animator.getAnimatedIntValue(), 0); }}); } mScrollAnimator.setIntValues(startScrollX, targetScrollX); mScrollAnimator.start(); } // Now animate the indicator mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); }Copy the code

StartScrollX is the current slide distance, calculateScrollXForTab I posted in the code above, is the distance after the slide if the situation changes, if (startScrollX! = targetScrollX) is determine whether slide, slide listening there are written into the position of the scrollTo (animator. GetAnimatedIntValue (), 0). The mainest is mScrollAnicmator. SetIntValues (startScrollX targetScrollX); Although I don’t understand to the operation of the mScrollAnicmator, but I think this is a record of operation, then click the position after newPosition is, the last call animateIndicatorToPosition

void animateIndicatorToPosition(final int position, int duration) { if (mIndicatorAnimator ! = null && mIndicatorAnimator.isRunning()) { mIndicatorAnimator.cancel(); } final boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; final View targetView = getChildAt(position); if (targetView == null) { // If we don't have a view, just update the position now and return updateIndicatorPosition();  return; } 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) { ValueAnimatorCompat animator = mIndicatorAnimator = ViewUtils.createAnimator(); animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); animator.setDuration(duration); animator.setFloatValues(0, 1); animator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimatorCompat animator) { final float fraction = animator.getAnimatedFraction(); setIndicatorPosition( AnimationUtils.lerp(startLeft, targetLeft, fraction), AnimationUtils.lerp(startRight, targetRight, fraction)); }}); animator.setListener(new ValueAnimatorCompat.AnimatorListenerAdapter() { @Override public void onAnimationEnd(ValueAnimatorCompat animator) { mSelectedPosition = position; mSelectionOffset = 0f; }}); animator.start(); }}Copy the code




image.png

Animationutils. lerp(startLeft, targetLeft, fraction) AnimationUtils.lerp(startRight, targetRight, fraction)); SetUpdateListener is an animation update listener. In other words, it calls the setIndicatorPosition to redraw until the animation ends. There must be a key point in the lerp.

static int lerp(int startValue, int endValue, float fraction) {
        return startValue + Math.round(fraction * (endValue - startValue));
    }
Copy the code

I don’t know what this fraction is, and then I don’t know how to use this 24dp, the best I can do is I know that the left and right points after sliding are set here, right





image.png

As for how to calculate get, I too vegetables, do not understand, hope to have god to see can guide. That’s the end of the chapter. Simple custom sliders can only be used with tabLayouts that don’t slide, and I’ll update this post if I figure out how to do it inside the source code.