This article has been authorized wechat public account: hongyangAndroid original first.

ChangeTabLayout is achieved by imitating the TabLayout effect on the main interface of LetV LIVE App. I hope you can support it more.

1. Effect display and description

The original rendering

The original renderings are too large to Gif, so the MP4 effect videos recorded have been placed in the Preview folder of the root directory of the project. If you are interested, you can check it out. (Hd no code oh ~)

Realize the effect drawing

ChangeTabLayoutIn the open state

  • The color size of text changes when switching vertically.
  • Horizontal switching, text gradient and image changes.

ChangeTabLayoutIn the stowed state

  • The picture changes when the vertical direction is switched.
  • Click on theChangeTabLayoutTo switch to the enabled state.

2. The analysis

First take a look at the hierarchy using the HierarchyViewer:

The TabLayout is a ScrollView, and the content area is a vertical ViewPager nested with a horizontal ViewPager. Image color changes are achieved using two ImageViews superimposed on each other. With that in mind, we have a general idea. Of course, we are not always the same, so we can handle it in our own way.

Finally, post a final image of what I achieved:

It can be seen that my structure is simpler, because I use the custom Drawable to achieve the effect of the image part, so there is no need to superimpose an ImageView, which eliminates the FrameLayout of the outer layer. Secondly, I use Canvas to draw the indicator. So we’re missing the outer RelativeLayout.

3. Preparation

  • I found VerticalViewPager on Github, the biggest gay dating site in the world. Unfortunately, this project is very old. For example, setOnPageChangeListener is out of date. Sometimes we need to add more than one listener to work at the same time. So I refer to the idea of VerticalViewPager, re-modified the existing ViewPager (25.1.0) source code. (It’s a delicate job.)

  • Secondly, I remembered the SmartTabLayout I used and found it very convenient to use. So I read its source code in advance. So the implementation structure of this project borrows a lot from it.

  • As for the changed part of the picture, I found this custom Drawables. After studying the code, I added the judgment of vertical direction based on the requirements and removed the redundant code part.

Thanks for sharing! Then everything is ready, let’s do it!!

4. Implement the process

After the preparation, first figure out what we’re missing, then all that’s left is the text part, the indicator part, and the container that holds those components.

1. Text

According to the observation renderings, the change of the text is the part covered by the indicator, and the text becomes white. And as the page moves vertically, the text changes in size. Of course, when the page horizontal switch, text gradient we can use setAlpha to achieve.

ChangeTextView core code

public ChangeTextView(Context context,  AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextAlign(Paint.Align.LEFT);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        mPaint.setXfermode(mode);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        resetting();

        Bitmap srcBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        Canvas srcCanvas = new Canvas(srcBitmap);

        RectF rectF;
        // Text changes color with indicator position
        if (level == 10000 || level == 0) {
            rectF = new RectF(0.0.0.0);
        }else if (level == 5000) {
            rectF = new RectF(0.0, getMeasuredWidth(), getMeasuredHeight());
        }else{
            float value = (level / 5000f) - 1f;

            if(value > 0){
                rectF = new RectF(0, getMeasuredHeight() * value + indicatorPadding, getMeasuredWidth(), getMeasuredHeight());
            }else{
                rectF = new RectF(0.0, getMeasuredWidth(), getMeasuredHeight() * (1 - Math.abs(value)) - indicatorPadding);
            }
        }

        srcCanvas.save();
        srcCanvas.translate(0, (getMeasuredHeight() - mStaticLayout.getHeight()) / 2);
        mStaticLayout.draw(srcCanvas);
        srcCanvas.restore();

        mPaint.setColor(selectedTabTextColor);
        srcCanvas.drawRect(rectF, mPaint);
        canvas.drawBitmap(srcBitmap, 0.0.null);
    }

    private void resetting() {float size;
        // The font changes with sliding
        if (level == 5000) {
            size = textSize * 1.1f; // The maximum size is 1.1 times the default size
        }else if(level == 10000 || level == 0){
            size = textSize * 1f;
        }else{
            float value = (level / 5000f) - 1f;
            size = textSize + textSize * (1 - Math.abs(value)) *0.1f;
        }

        mTextPaint.setTextSize(size);
        mTextPaint.setColor(defaultTabTextColor);
        int num = (getMeasuredWidth() - indicatorPadding) / (int) size; // The number of words that can be placed in a line

        mStaticLayout = new StaticLayout(text, 0, text.length() > num * 2 ?  num * 2 : text.length(), mTextPaint, getMeasuredWidth() - indicatorPadding,
                Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, false);

    }Copy the code

The calculation part will not be introduced, the text changes mainly use the SRC_IN mode of PorterDuffXfermode we commonly. That is, take two layers and draw the intersection to show the upper layer. The diagram below:

For example, if the text is black and the mask is light blue, the overlapping text will be light blue. The rest of the mask is not displayed. The schematic diagram is as follows:

So we can control the color change of the text by changing the size of the RectF.

So in order to be able to display multiple lines of text, and at the same time allow text to be wrapped with size. I used StaticLayout to do that. It’s easy to use.

2. Indicator part

The indicators, background, and shadows are all inside the ScrollView’s child LinearLayout.

ChangeTabStrip core code:

class ChangeTabStrip extends LinearLayout{

    public ChangeTabStrip(Context context, @Nullable AttributeSet attrs) {
        super(context);
        setWillNotDraw(false);
        setOrientation(VERTICAL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawShadow(canvas);
        drawBackground(canvas);
        drawDecoration(canvas);
    }

    private void drawDecoration(Canvas canvas) {
        final int tabCount = getChildCount();

        if (tabCount > 0) {
            View selectedTab = getChildAt(selectedPosition);
            int selectedTop = selectedTab.getTop();
            int selectedBottom = selectedTab.getBottom();
            int top = selectedTop;
            int bottom = selectedBottom;

            if (selectionOffset > 0f && selectedPosition < (getChildCount() - 1)) {

                View nextTab = getChildAt(selectedPosition + 1);
                int nextTop = nextTab.getTop();
                int nextBottom = nextTab.getBottom();
                top = (int) (selectionOffset * nextTop + (1.0f - selectionOffset) * top);
                bottom = (int) (selectionOffset * nextBottom + (1.0f - selectionOffset) * bottom); } drawIndicator(canvas, top, bottom); }}/** * draw a left shadow */
    private void drawShadow(Canvas canvas){
        final float width = shadowWidth * (1 - selectionOffsetX);
        LinearGradient linearGradient = new LinearGradient(0, getHeight(), width, getHeight(), new int[] {shadowColor, Color.TRANSPARENT}, new float[]{shadowProportion, 1f}, Shader.TileMode.CLAMP);
        shadowPaint.setShader(linearGradient);
        canvas.drawRect(0.0, width, getHeight(), shadowPaint);
    }

    /** * draw the background */
    private void drawBackground(Canvas canvas){
        final float width = getWidth() * selectionOffsetX;
        canvas.drawRect(0.0, width, getHeight(), backgroundPaint);
    }

    /** * draw indicator */
    private void drawIndicator(Canvas canvas, int top, int bottom) {

        final float width = getWidth() * selectionOffsetX;
        top = top + indicatorPadding;
        bottom = bottom - indicatorPadding;

        float leftBorderThickness = this.leftBorderThickness - getWidth() * (1 - selectionOffsetX);
        if(leftBorderThickness < 0){
            leftBorderThickness = 0;
        }

        borderPaint.setColor(leftBorderColor);
        canvas.drawRect(0, top, leftBorderThickness, bottom, borderPaint); indicatorPaint.setColor(indicatorColor); indicatorRectF.set(leftBorderThickness, top, width, bottom); canvas.drawRect(indicatorRectF, indicatorPaint); }}Copy the code

There’s nothing special here, just constantly drawing new positions based on the ViewPager movement.

3. TabLayout part

Create a child container first:

ChangeTabStrip tabStrip = new ChangeTabStrip(context, attrs);
addView(tabStrip , LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);Copy the code

Based on the PagerAdapter passed in, use the Adapter.getCount () method to create the corresponding number of TabViews and add them to the ChangeTabStrip. The simplified code is as follows:

 private void populateTabStrip() {
        final PagerAdapter adapter = viewPager.getAdapter();

        int size = adapter.getCount();
        for (int i = 0; i < size; i++) {
            LinearLayout tabView = createTabView(adapter.getPageTitle(i), icon[i], 0);

            if (tabView == null) {
                throw new IllegalStateException("tabView is null.");
            }

            tabStrip.addView(tabView);

            if (i == viewPager.getCurrentItem()) { // The TabView corresponding to the current Page is selected
                ChangeTextView textView = (ChangeTextView) tabView.getChildAt(1);
                textView.setLevel(5000); }}}protected LinearLayout createTabView(CharSequence title, int icon) {

        LinearLayout mLinearLayout = new LinearLayout(getContext());
        mLinearLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, tabViewHeight));

        ImageView imageView = new ImageView(getContext());

        RevealDrawable drawable = new RevealDrawable(DrawableUtils.getDrawable(getContext(), icon), DrawableUtils.getDrawable(getContext(), selectIcon), RevealDrawable.VERTICAL);       

        imageView.setImageDrawable(drawable);

        ChangeTextView textView = new ChangeTextView(getContext(), attrs);
        textView.setText(title.toString());        

        mLinearLayout.addView(imageView);
        mLinearLayout.addView(textView);
        return mLinearLayout;
}Copy the code

Listen for the vertical ViewPager

viewPager.addOnPageChangeListener(new InternalViewPagerListener());


private class InternalViewPagerListener implements VerticalViewPager.OnPageChangeListener {

        private int scrollState;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            int tabStripChildCount = tabStrip.getChildCount();
            if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
                return;
            }
            tabStrip.onViewPagerPageChanged(position, positionOffset); // Control indicator position
            scrollToTab(position, positionOffset); // Scroll the ScrollView to the corresponding position
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            scrollState = state;
        }

        @Override
        public void onPageSelected(int position) {
            if (scrollState == ViewPager.SCROLL_STATE_IDLE) {
                scrollToTab(position, 0);
            }
            page = position; // Record the location

            // Change the text display
            for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) {
                ChangeTextView textView = (ChangeTextView) ((LinearLayout) tabStrip.getChildAt(i)).getChildAt(1);
                if (position == i) {
                    textView.setLevel(5000);
                }else {
                    textView.setLevel(0); }}}}private void scrollToTab(int tabIndex, float positionOffset) {

        final int tabStripChildCount = tabStrip.getChildCount();
        if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) {
            return;
        }

        LinearLayout selectedTab = (LinearLayout) getTabAt(tabIndex);

        int titleOffset = tabViewHeight * 2;
        int extraOffset = (int) (positionOffset * selectedTab.getHeight());

        int y = (tabIndex > 0 || positionOffset > 0)? -titleOffset :0;
        int start = selectedTab.getTop();
        y += start + extraOffset;

        scrollTo(0, y);
    }Copy the code

Dynamic parts of pictures and text when sliding vertically:

private void scrollToTab(int tabIndex, float positionOffset) {

        LinearLayout selectedTab = (LinearLayout) getTabAt(tabIndex);

        if (0f <= positionOffset && positionOffset < 1f) {
            if(! tabLayoutState){// Close the state of the image change
                ImageView imageView = (ImageView) selectedTab.getChildAt(0);
                ((RevealDrawable)imageView.getDrawable()).setOrientation(RevealDrawable.VERTICAL);
                imageView.setImageLevel((int) (positionOffset * 5000 + 5000));
            }
            ChangeTextView textView = (ChangeTextView) selectedTab.getChildAt(1);
            textView.setLevel((int) (positionOffset * 5000 + 5000));
        }

        if(! (tabIndex +1 >= tabStripChildCount)){
            LinearLayout tab = (LinearLayout) getTabAt(tabIndex + 1);

            if(! tabLayoutState){ ImageView img = (ImageView) tab.getChildAt(0);
                ((RevealDrawable)img.getDrawable()).setOrientation(RevealDrawable.VERTICAL);
                img.setImageLevel((int) (positionOffset * 5000));
            }
            ChangeTextView text = (ChangeTextView) tab.getChildAt(1);
            text.setLevel((int) (positionOffset * 5000)); }}Copy the code

Dynamic changes of pictures and text when sliding horizontally:

final int tabStripChildCount = tabStrip.getChildCount();
            if (tabStripChildCount == 0 || page < 0 || page >= tabStripChildCount) {
                return;
            }

            LinearLayout selectedTab = (LinearLayout) getTabAt(page);
            ImageView imageView = (ImageView) selectedTab.getChildAt(0);
            ((RevealDrawable)imageView.getDrawable()).setOrientation(RevealDrawable.HORIZONTAL);
            if (0f < positionOffset && positionOffset <= 1f) {
                imageView.setImageLevel((int) ((1 - positionOffset) * 5000 + 5000));
            }

            for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) {
                ChangeTextView textView = (ChangeTextView) ((LinearLayout) tabStrip.getChildAt(i)).getChildAt(1);
                if (0f < positionOffset && positionOffset <= 1f) {
                    textView.setAlpha((1 - positionOffset)); // Text gradient
                    if(positionOffset > 0.9 f) {// Hide when greater than 0.9
                        textView.setVisibility(INVISIBLE);
                    }else{
                        textView.setVisibility(VISIBLE);
                    }
                }
            }

tabStrip.onViewPagerPageChanged(positionOffset);// Control indicator, background, shadow changes.Copy the code

At this point, the general flow is complete.

5. Fix some minor problems

1. Fill the screen

This is what it looks like if the tabViews are too small to cover the entire screen.

This is going to look a little awkward. I set the height to MATCH_PARENT but it didn’t work.

addView(tabStrip, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);Copy the code

Simply add setFillViewport(true). As the name implies, this property allows the component size in the ScrollView to fill up when it is insufficient.

2. Click on the question

When ChangeTabLayout is retracted, the text is hidden, but it still uses gestures. When retracting, we cannot click on the ViewPager below and can slide ChangeTabLayout.

My solution is to calculate the text section of the area and determine whether to intercept.

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(tabLayoutState){
            return super.onTouchEvent(event);
        }else {
            final int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN: // Click not to block when folding, pass to the lower layer
                    return false;
                case MotionEvent.ACTION_MOVE: // When retracting, slide text section intercepts
                    if(tabImageHeight + (int) (20 * density) < event.getRawX()){
                        return true;
                    }
                    break;
            }
            return super.onTouchEvent(event); }}Copy the code

3. The text is displayed abnormally

When you click ChangeTabLayout to switch the page, the following abnormal display will sometimes occur.

Cause I use the viewager. SetCurrentItem (I) method to switch. The result is a smooth scroll in the ViewPager switch, and the listener onPageScrolled receives a partial page feedback value. The easy solution is to use viewpager.setCurrentitem (I, false) to toggle.

However stubborn I choose not to do (torture each other to the white head, sad determined not to let go ~~). Came up with a solution like this.

Change flag to true when you touch ViewPager. Set to false when you click toggle. Judge before each change.

/** * tabView toggle whether text changes in real time */
private boolean flag = false;

private class ViewPagerTouchListener implements OnTouchListener{

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    flag = true;
                    break;
            }
            return false; }}if(flag){
     ChangeTextView textView = (ChangeTextView) selectedTab.getChildAt(1);
     textView.setLevel((int) (positionOffset * 5000 + 5000));
  }Copy the code

4. OnPageScrolled listening is abnormal

The onPageScrolled listening of a ViewPage swiped horizontally in portrait is not working properly.

Normal printing looks like this :(n pages – 0.0 at the end of the slide)

The result in portrait is this:

Please inform me of this abnormal print. Thank you!

Solutions:

 public void setPageScrolled(int p, int position, float positionOffset) {
        // Unify abnormal data
        if (positionOffset > 0.99 && positionOffset < 1){
            positionOffset = 0;
            position = position + 1;
        }else if (positionOffset < 0.01 && positionOffset > 0.00001){
            positionOffset = 0; }}Copy the code

It’s been a few days since we released it, and we’ve been getting feedback on our questions. Thank you very much! Suddenly feel fine or everyone fine, I am still too thick… While I have time to sort out the above implementation ideas, I hope you are interested in help.

Source code here, a lot of praise star oh ~~