史上最巧妙自定义tablayout指示器

xiaoxiao2021-02-28  98

国际惯例,无图无真相

首先我们先过几个概念,老手这个请自行跳过。

Android的View显示在界面上需要三步:测量,定位和绘制。

第一步:测量,View的measure方法

这个方法用来测量View显示的宽高值。这个宽高值是基于View**自身宽高,再加上父View的约束**得到的。这个约束使用MeasureSpec类传递。

@Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }

measure方法是final型的,子类需要重写的是onMeasure方法,这里做了两件事:真正测量宽高值;保存宽高值。保存操作是调用setMeasuredDimension方法,以供后续步骤使用。 在View的onMeasure方法中,使用getDefaultSize方法获取在具体size和具体measureSpec下调整后的最终size,并调用setMeasuredDimension方法保存。 要是有子View,需要在onMeasure方法中调用ViewGroup的measureChild方法。

第二步:定位,View的layout方法

这个方法用来将View(子View)放在确定的位置。这时View的左上右下的坐标值就存在了。 这个方法是final型的,子类需要重写的是onLayout方法,

protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); }

layout方法最初传入的右下参数,就是measure方法中保存的值。 要是有子View,需要在onLayout方法中调用子View的layout方法。

第三步:绘制,View的draw方法

这个方法用来绘制view内容。包括自身及子View。 draw方法中将绘制过程分六步:背景;阴影层(if nesessary);view自身;子View;阴影边缘(if nesessary);装饰部分(如前景色,滚动条)。

其中,view自身绘制在onDraw方法中实现,子View绘制在dispatchDraw方法中实现。View类中这两个方法均是空实现,ViewGroup类中仅对dispatchView添加具体实现,即依次调用子View的draw方法(用drawChild方法封装)。 draw方法是final型的,子类需要重写的是onDraw方法,来完成自身的绘制。 要是有子View,一般直接使用ViewGroup的dispatchDraw方法就可以了,不需要重写。

private class SlidingTabStrip extends LinearLayout { private int mSelectedIndicatorHeight; private final Paint mSelectedIndicatorPaint; int mSelectedPosition = -1; float mSelectionOffset; private int mIndicatorLeft = -1; private int mIndicatorRight = -1; private ValueAnimatorCompat mIndicatorAnimator; SlidingTabStrip(Context context) { super(context); setWillNotDraw(false); mSelectedIndicatorPaint = new Paint(); } //省略部分代码 @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //省略部分代码 } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); //省略部分代码 } @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); } } }

这里我们知道private class SlidingTabStrip 是私有方法,而且 private final SlidingTabStrip mTabStrip; 是final,我们没有通过重写继承更改。那么要怎么去修改呢?有些人可能会想到反射,但是反射怎么才能最简单的修改呢?

如果对反射不是很熟悉的,可以参考下这一篇博客 你必须掌的握反射用法

上面的关键代码主要是其draw(Canvas canvas)方法,我们发现他传入的参数是canvas ,我们能否拿到这个对象,然后就可以在外部使用canvas进行画图了。

思路有了,怎么实现呢?

通过源码我们可以知道,TabLayout 添加Tab最终是添加到我们上面说的mTabStrip里面

private void addTabView(Tab tab) { final TabView tabView = tab.mView; mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs()); }

而TabLayout的子view就是我们上面说的SlidingTabStrip ,通过mTabLayout.getChildAt(0)获取。然后我们知道view实现了drawable的回调接口

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { }

于是实现自定义drawable代理类如下:

public class ProxyDrawable extends Drawable { View view; Paint paint; float paddingLeft ; float paddingTop; public ProxyDrawable(View view) { this.view = view; paint = new Paint(); paint.setColor(0xFF6DA9FF); float density = view.getResources().getDisplayMetrics().density; //这两个留白可以根据需求更改 paddingLeft = 0 * density; paddingTop = 5 * density; } @Override public void draw(@NonNull Canvas canvas) { //这里通过反射获取SlidingTabStrip的两个变量,源代码画的是下划线,我们现在画的是带圆角的矩形 int mIndicatorLeft = getIntValue("mIndicatorLeft"); int mIndicatorRight = getIntValue("mIndicatorRight"); int height = view.getHeight(); int radius = height / 2; if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { canvas.drawRoundRect(new RectF(mIndicatorLeft + (int)paddingLeft, (int)paddingTop, mIndicatorRight - (int)paddingLeft, height - (int)paddingTop), radius, radius, paint); } } int getIntValue(String name) { try { Field f = view.getClass().getDeclaredField(name); f.setAccessible(true); Object obj = f.get(view); return (Integer) obj; } catch (Exception e) { e.printStackTrace(); } return 0; } @Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } }

实现代码如下即可实现指示器修改,存在的问题是系统画的下划线仍然存在,需要在布局将其隐藏

View view1 = mTabLayout.getChildAt(0); view1.setBackgroundDrawable(new ProxyDrawable(view1));

其中app:tabBackground=”@null”去除tab点击的阴影效果 app:tabIndicatorColor=”@null”去除tab的下划线 布局代码对比如下:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.philos.tablayoutdrawproxydemo.MainActivity" android:orientation="vertical"> <android.support.design.widget.TabLayout android:id="@+id/tab0" android:layout_marginBottom="16dp" android:background="@color/colorPrimaryDark" app:tabBackground="@null" app:tabIndicatorColor="@null" app:tabSelectedTextColor="@color/colorAccent" android:layout_width="match_parent" android:layout_height="45dp"/> <android.support.design.widget.TabLayout android:id="@+id/tab" android:layout_marginBottom="16dp" android:background="@color/colorPrimaryDark" app:tabBackground="@null" app:tabSelectedTextColor="@color/colorAccent" android:layout_width="match_parent" android:layout_height="45dp"/> <android.support.design.widget.TabLayout android:id="@+id/tab1" android:layout_marginBottom="16dp" app:tabMode="fixed" app:tabMaxWidth="150dp" app:tabPadding="8dp" android:background="@color/colorPrimaryDark" app:tabBackground="@null" app:tabIndicatorColor="@null" app:tabSelectedTextColor="@color/colorAccent" android:layout_width="match_parent" android:layout_height="45dp"/> </LinearLayout>

这样就巧妙实现了TabLayout的指示器更改,本来昨晚写这篇博客的,没想到我们家喵大人过来霸占了电脑,然后只能早上起来写了。。。。标题有点过了,请大家海涵。。。。。

附一张我们家起司的萌照

转载请注明原文地址: https://www.6miu.com/read-28888.html

最新回复(0)