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:
- Inherit the ViewGroup
- Overrides onMeasure to calculate the size of the child control to determine the size of the parent control
- 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:
- Boundary restrictions
- 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:
- The background of the child control automatically changes with its size
- The background slides automatically as the viewPager scrolls
- 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?
-
OnDraw draws content. OnDraw is the actual thing to care about, which is that all the draws are here.
-
“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.