This article has authorized Yu Gang to say the public number

FlowLayout inherits from ViewGroup, it can quickly help you to realize Tablayout and Label labels, including a variety of effects, help you quickly realize APP UI functions, let you focus on code architecture, say goodbye to tedious UI.

If you want to write your own, check out the following articles

Implement a customizable TabFlowLayout(a) – measurement and layout

Implement a customizable TabFlowLayout(two) – to achieve scrolling and smooth transition

Implement a customizable TabFlowLayout(three) — dynamic data add and common interface encapsulation

Implement a customizable TabFlowLayout(four) – combined with ViewPager, achieve cool effect

Implement a customizable TabFlowLayout – Principles

Implement a customizable TabFlowLayout – documentation

FlowLayout and Recyclerview to achieve double table linkage

If you also want to implement banners quickly, you can use this library github.com/LillteZheng…

A correlation

allprojects {
    repositories {
       ...
        maven { url 'https://jitpack.io'}}}Copy the code

The latest version is subject to the project: To achieve a customizable FlowLayout

implementation 'com. Making. LillteZheng: FlowHelper: v1.17'
Copy the code

If you want to support AndroidX, if your project already has the following code, directly associated with:

android.useAndroidX=true
# automatic support for AndroidX third-party libraries
android.enableJetifier=true
Copy the code

The effect

First, is the effect of TabFlowLayout, its layout support vertical and horizontal two ways, first look at the effect of support:

No ViewPager Combining with the ViewPager
TabFlowLayout vertical, RecyclerView linkage effect

In addition to TabFlowLayout, there are LAbelFlowLayout tabbed layout, support for line wrapping and display more

LabelFlowLayout LabelFlowLayout shows more

3. Principle description

Here mainly to TabFlowLayout to illustrate, as for LabelFlowLayout, I believe we read the analysis, also know how to achieve.

3.1 Measurement and layout

From the above effect, there are a lot of customization options, such as inheriting LinearLayout or HorizontalScrollView… , but in fact directly inherit ViewGroup to dynamic measurement more fragrant; First, the steps are simple:

  1. Inherit the ViewGroup
  2. Overrides onMeasure to calculate the size of the child control to determine the size of the parent control
  3. Rewrite onLayout to determine the layout of the child controls

Directly look at the second step, because it is horizontal, in the measurement, need to determine the width of the child control accumulation, and height, then take the child control, the largest one, the code is shown as follows:

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int childCount = getChildCount(); int width = 0; int height = 0; /** * calculate width, since width is the sum of all child controls, regardless of mode */for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE){
                continue; } measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); Int CW = child.getMeasuredWidth() + params.leftMargin + params.rightMargin; int ch = child.getMeasuredHeight() + params.topMargin + params.bottomMargin; width += cw; Height = math. Max (height, ch); }if (MeasureSpec.EXACTLY == heightMode) {
            height = heightSize;
        }
        setMeasuredDimension(width, height);
    }
Copy the code

SetMeasuredDimension (Width, height); setMeasuredDimension(Width, height); Assign to the parent control.

In the third step, rewrite onLayout to determine the layout of the child control. Since it is landscape, only the left of the child control is accumulated.

  @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
       int count = getChildCount();
       int left = 0;
       int top = 0;
       for(int i = 0; i < count; i++) { View child = getChildAt(i); MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); int cl = left + params.leftMargin; int ct = top + params.topMargin; int cr = cl + child.getMeasuredWidth() ; int cb = ct + child.getMeasuredHeight(); Left += child.getMeasuredWidth() + params.leftMargin + params.rightMargin; child.layout(cl, ct, cr, cb); }}Copy the code

In this way, a simple horizontal TabFlowLayout is done, let’s write some controls experiment:

    <com.zhengsr.tablib.TabFlowLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="# 15000000"
        >

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="001"/ >... </com.zhengsr.tablib.TabFlowLayout>Copy the code

Effect:

TabFlowLayout is a custom TabFlowLayout that measures and layouts

3.2 Achieve rolling and smooth transitions

In the previous section, we used FlowLayout to implement measurement and layout. This time, we created a new class ScrollFlowLayout that implements the scrolling logic. View event passing can be described as follows:

When a control is clicked, it passes down like this: Activity –> Window –> viewGroud –> View. The first one, of course, is disPatchTouchEvent; We know from the source that if we return true for onInterceptTouchEvent, the parent control takes over the current touch event, does not pass it down, and instead calls back its own onTouchEvent method.

Since we inherit from ViewGroup, we need to override its onInterceptTouchEvent method:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            caseMotionEvent.ACTION_DOWN: mLastX = ev.getX(); MMoveX = ev.getx ();break;

            case MotionEvent.ACTION_MOVE:
                float dx = ev.getX() - mLastX;
                ifMath.abs(dx) >= mTouchSlop) {// Take over the touch event by the parent controlreturn true;
                }
                mLastX = ev.getX();
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
Copy the code

In the above code, you can take over the Touch event, and then in the onTouchEvent, you can get the offset of the move, so how do you implement the View itself move? That’s right, ScrollerBy and ScrollerTo, they just change the contents of the View and they don’t change the coordinates of the View, which is exactly what we need, but notice that the left slide is positive and the right slide is negative.

  • ScrollerTo(int x,int y) absolute coordinates move, with the origin as the reference point
  • ScrollerBy(int x,int y) moves relative to the previous coordinate
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            caseACTION_MOVE: //scroller is negative and left is positive int dx = (int) (mmovex-event.getx ()); scrollBy(dx, 0); mMoveX = event.getX();break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;

        }
        return super.onTouchEvent(event);
    }

Copy the code

The effect is as follows:

But there seem to be some problems:

  1. Boundary restrictions
  2. Not rolling smoothly enough

A TabFlowLayout that can be customized (2) – to achieve scrolling and smooth transition

3.3 Adding dynamic data and encapsulating common interfaces

Here refer to FlowLayout FlowLayout of Hongyang

The top TAB may have unread messages, or different controls. Therefore, layoutId must be present. Datas must also be present, and this data type must be generic. So, roughly minimalist code can be written like this:

/** * @author by zhengshaorui on 2019/10/8 * Describe: */ public abstract class TabAdapter<T> {private int mLayoutId; private List<T> mDatas; public TabAdapter(int layoutId, List<T> data) { mLayoutId = layoutId; mDatas = data; } /** * Get the number * @return
     */
    int getItemCount() {returnmDatas == null ? 0 : mDatas.size(); } /** * get id * @return
     */
    int getLayoutId() {returnmLayoutId; } /** * get data * @return
     */
    List<T> getDatas() {returnmDatas; } /** ** Public data to external * @param view * @param data * @param position */ public abstract voidbindView(View view,T data,int position); /** * Notify data changes */ public voidnotifyDataChanged() {if(mListener ! = null) { mListener.notifyDataChanged(); } /** * build a listener to change data */ public AdapterListener mListener; voidsetListener(AdapterListener listener){ mListener = listener; }... }Copy the code

So add a new setAdapter to TabFlowLayout and set the data into it:

TabFlowLayout flowLayout = findViewById(R.id.triflow);
flowLayout.setAdapter(new TabFlowAdapter<String>(R.layout.item_msg,mTitle2) {

    @Override
    public void bindView(View View, String data, int position) {// Set textView's text and colorsetText(view,R.id.item_text,data) .setTextColor(view,R.id.item_text,Color.BLACK); }});Copy the code

But how does it work inside? You just get layoutId and count from the Adapter and addView

removeAllViews();
TabAdapter adapter = mAdapter;
int itemCount = adapter.getItemCount();
for (int i = 0; i < itemCount; i++) {
    View view = LayoutInflater.from(getContext()).inflate(adapter.getLayoutId(),this,false);
    adapter.bindView(view,adapter.getDatas().get(i),i);
    configClick(view,i);
    addView(view);
}

Copy the code

The effect is as follows:

For details, see this article: Implementing a customizable TabFlowLayout(3) – Dynamic data addition and common interface encapsulation

3.3.4 Combine with ViewPager to achieve cool effect

The first effect to be achieved is as follows:

As you can see, several effects are implemented:

  1. The background of the child control automatically changes with its size
  2. The background slides automatically as the viewPager scrolls
  3. When moving to the middle, if there is extra data behind, keep the background in the middle and the content moving

First, implement a red background box; First of all, think about the implementation of canvas in viewGroup, is it onDraw(Canvas Canvas) or dispatchDraw(Canvas canvas)? DispathDraw, why?

  1. OnDraw draws content. OnDraw is the actual thing to care about, which is that all the draws are here.

  2. “DispatchDraw” is only useful for viewgroups. It’s usually interpreted as drawing a child View that inherits drawable, and the View component draws with draw(Canvas Canvas) method, The Drawable background is drawn first, followed by onDraw and then the dispatchDraw method. DispatchDraw is sent to the component to draw. But a View doesn’t have child views, so dispatchDraw doesn’t make sense to it.

So, when you customize a ViewGroup, if the ViewGroup doesn’t have a background, you don’t call onDraw, you just call dispatchDraw, and it goes in the normal order if it has a background. (don’t believe it? You can remove your TabFlowLayout background and draw it in onDraw to see if it works.

So let’s get the size of the first child view and determine the rect:

View child = getChildAt(0);
if(child ! MarginLayoutParams = (MarginLayoutParams) child.getLayOutParams (); mRect.set(getPaddingLeft()+params.leftMargin, getPaddingTop()+params.topMargin, child.getMeasuredWidth()-params.rightMargin, child.getMeasuredHeight() - params.bottomMargin); }Copy the code

Then draw the rounded rectangle in dispatchDraw:

@override protected void dispatchDraw(Canvas Canvas) {draw a Canvas. DrawRoundRect (mRect, 10, 10, mPaint); super.dispatchDraw(canvas); }Copy the code

The effect is as follows:

Now, how do I get this background to follow the viewPager?

You can get the onPageScrolled method from the viewPager page listener:

public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
Copy the code

The three parameters are described as follows:

  • Position: the index of the current page. Interestingly, if you swipe right, position indicates the current page. If you swipe left, the current page is subtracted by 1.
  • PositionOffset: percentage of current page movement between [0,1]; Right slide 0-1, left slide 1-0;
  • PositionOffsetPixels: Pixels moving on the current page

As you can see from the above, we only need position and positionOffset, i.e. the previous left offset is the offset to be moved, plus the child view’s width change:

@Override
public void onPageScrolled(int position, floatPositionOffset, int positionOffsetPixels) {/** * position Specifies the index of the current page. * positionOffset Percentage of current page movement * positionOffsetPixels Pixels of current page movement */if(position < getChildCount() -1) {// Last final view lastView = getChildAt(position); CurView = getChildAt(position + 1); // The left offsetfloatleft = lastView.getLeft() + positionOffset * (curView.getLeft() - lastView.getLeft()); // The right side represents the width changefloatright = lastView.getRight() + positionOffset * (curView.getRight() - lastView.getRight()); mRect.left = left; mRect.right = right; postInvalidate(); }}Copy the code

This will allow you to move around. See this article for details: Implementing a customizable TabFlowLayout(4) – Combined with ViewPager to achieve cool effects

extension

Understand the TabFlowLayout implementation process, then the implementation of LabelFlowLayout can also according to the gourd gourd. When measuring, determine whether to wrap, and then in onLayout to row the position of the child control.

Here’s how LabelFlowLayout shows more fading effects.

First of all, when we limit it to 2 rows, we need to show more effects. Here, to facilitate customization, we add a layoutId for the user to configure.

So how do I get it down here?

In order to get the correct width and height of the view, it needs to be given to LabelFlowLayout to assist in measurement and increase the view height by half for display. Therefore, in onMeasure, it can be written as follows:

/** * layoutId requires the parent control, LabelFlowLayout, to get the correct measuredXXX width and height, */if(mView ! = null) { measureChild(mView, widthMeasureSpec, heightMeasureSpec); // Add 1/2 of it to blur mViewHeight += mView.getMeasuredHeight() /2;setMeasuredDimension(mLineWidth, mViewHeight);
}
Copy the code

So what about the blur effect? You can actually start with Paint.

First, convert the View to a bitmap, and then set a shader for paint with the top part of the color transparent and the bottom part matching the background color as follows:

Mview.layout (0, 0, getWidth(), mView.getMeasuredHeight()); mView.buildDrawingCache(); mBitmap = mView.getDrawingCache(); /** * add a shader, */ Shader Shader = new LinearGradient(0, 0, 0, getHeight(), color.transparent, mShowMoreColor, Shader.TileMode.CLAMP); mPaint.setShader(shader); mBitRect.set(l, getHeight() - mView.getMeasuredHeight(), r, getHeight());Copy the code

Then dispatchDraw the effect and the bitmap to it:

@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    if (isLabelMoreLine() && mBitmap != null) {
        canvas.drawPaint(mPaint);
        canvas.drawBitmap(mBitmap, mBitRect.left, mBitRect.top, null);
    }

}
Copy the code

At this point, the principle of FLowHelper is basically analyzed, you can first implement a, and then refer to the engineering code.