在实际开发中,咱们经常需求运用viewpage调配SlideTabLayout联动翻滚运用。以前google官方并没有SlideTabLayout相关Widget供咱们直接运用,因而需求咱们自界说实现。
后来google在material design中推出了TabLayout,但自界说才能缺乏,动画款式有限,往往不能满足咱们的要求。
不过在github上也许多关于SlideTabLayout,咱们能够去搜一下,有不是有许多start的。但相似ios西瓜视频主页那种,随着页面滑动,被选中的tab逐步扩大的作用确很少,或许运用流畅性不行。
![Demo](i.postimg.cc/0y0Ms5rJ/de…)
改造TabLayout
下面我但咱们在官方TabLayout的基础上,去增加一些事情接口供咱们扩展运用。
首先把TabLayout的源码复制出来,一共没几个文件很简单。
上面三个类是三种款式的滑动指示器,首要作用是依据滑动进度或许tab方位确认指示器的宽度和方位。
TabItem没什么,便是一个Tab声明,虽然是一个View但是终究不会被添加到TabLayout布局中,首要用于xml中声明在TabLayout中有几个TabItem,能够指定icon,text,customView。终究会被转换为TabView添加到TabLayout布局中。
TabLayout便是今日的主角,承继自HorizontalScrollView,具有了横向翻滚才能,直接子View是SlidingTabIndicator,它承继自LinearLayout,由于TabView都是线性横向排列,能够直接复用丈量和布局。
TabLayout整体视图结构还是比较简单的,一个HorizontalScrollView下包裹一个LinearLayout,LinearLayout里边是一个个TabView。
TabLayoutMadiator首要是为了兼容ViewPager2,这个就不多叙述。
本期咱们的扩展目标首要是在滑动的过程中对TabView进行处理。
使命分化
目前作用是TabView中的文字随着翻滚扩大或缩小,扩大缩小会涉及到一个问题,便是会改动View的显现巨细,假如TabView之间的间隔太小,还或许形成TabView重叠显现。为了更好的用户体会,在扩大缩小的一起我会取平移每一个TabView,确保每一个TabView文字之间的间隔始终是相等的。
- 目标1:动态缩放文字
- 目前2:动态水平移动TabView的方位
为了保持TabView之间的间隔不变,又会带来一个新的问题,会形成所有TabView的z总宽度是不等于LinearLayout的宽度,由于总是会有一个或许两个TabView是被缩放的,假如咱们设置的是一个扩大作用,那就会形成Tab总宽度大于LinearLayout的宽度,终究一个TabView会被截断。为了处理这个问题,咱们需求重写LinearLayout的逻辑,预留出额外的空间供TabView平移。预留出的空间是最宽的tabView乘以最大扩大系数。
- 目标3:从头丈量LinearLayout的宽度,预留空间
为了预留空间还会带来一个新的问题,便是翻滚到LinearLayout的最右边,或许会多出空白区域,由于咱们预留的是TabView的最大空间,假如当前选中的TabView不是宽度最大的那个,就会呈现空白区域。因而咱们需求动态改动最大翻滚间隔。
- 目标4:动态改动最大翻滚间隔
事情接口界说
界说tabLayout一些关键节点的事情接口,让外部操控器有机会去改动Tablayout的默许行为,从而达到定制的作用。
public interface ITabEventListener {
// 匹配形式
boolean matchMode(@TabLayout.Mode int mode);
// tabView的宽高确认
default void onTabViewLayout(@NonNull TabLayout.TabView tabView) {
}
default void onReMeasureChildren(@NonNull LinearLayout slidingTabIndicator, Consumer<Integer> action) {
}
default void onRelayoutChildren(@NonNull LinearLayout slidingTabIndicator) {
}
default void onUpdateProgress(@NonNull LinearLayout slidingTabIndicator, @NonNull TabLayout.TabView currentTab, @Nullable TabLayout.TabView nextTab, float progress) {
}
default int getScaleTabContentWidth(@NonNull TabLayout.TabView tabView, int originSize) {
return originSize;
}
default int getScaleTabContentHeight(@NonNull TabLayout.TabView tabView, int originSize) {
return originSize;
}
default int getScaleTabWidth(@NonNull TabLayout.TabView tabView, int originSize) {
return originSize;
}
default int getScaleTabHeight(@NonNull TabLayout.TabView tabView, int originSize) {
return originSize;
}
default int getScaleTabLeft(@NonNull TabLayout.TabView tabView, int left) {
return left;
}
default int getScaleTabTop(@NonNull TabLayout.TabView tabView, int top) {
return top;
}
default int transformScrollX(int x) {
return x;
}
default int transformScrollY(int y) {
return y;
}
}
在TabLayout中刺进事情点
能够改动最大翻滚间隔
@Override
public void scrollTo(int x, int y) {
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
x = tabEventListener.transformScrollX(x);
y = tabEventListener.transformScrollY(y);
}
super.scrollTo(x, y);
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
scrollX = tabEventListener.transformScrollX(scrollX);
scrollY = tabEventListener.transformScrollY(scrollY);
}
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
layout TabView,能够确认TabView的宽高
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
tabEventListener.onTabViewLayout(this);
}
}
提供给指示器计算方位运用,由于缩放TabView的一起,咱们也需求改动指示器的方位
int getScaleContentWidth() {
int result = getContentWidth();
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
result = tabEventListener.getScaleTabContentWidth(this, result);
}
return result;
}
int getScaleContentHeight() {
int result = getContentHeight();
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
result = tabEventListener.getScaleTabContentHeight(this, result);
}
return result;
}
int getScaleTabWidth() {
int width = getWidth();
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
width = tabEventListener.getScaleTabWidth(this, width);
}
return width;
}
int getScaleTabHeight() {
int height = getHeight();
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
height = tabEventListener.getScaleTabHeight(this, height);
}
return height;
}
int getScaleLeft() {
int left = getLeft();
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
left = tabEventListener.getScaleTabLeft(this, left);
}
return left;
}
int getScaleTop() {
int top = getTop();
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
top = tabEventListener.getScaleTabTop(this, top);
}
return top;
}
int getScaleRight() {
int right = getRight();
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
right = getScaleLeft() + tabEventListener.getScaleTabWidth(this, getWidth());
}
return right;
}
int getScaleBottom() {
int bottom = getBottom();
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
bottom = getScaleTop() + tabEventListener.getScaleTabHeight(this, getHeight());
}
return bottom;
}
linearLayout 从头丈量
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
tabEventListener.onReMeasureChildren(this, new Consumer<Integer>() {
@Override
public void accept(Integer width) {
setMeasuredDimension(width, getMeasuredHeight());
}
});
}
}
linearLayout 从头布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
tabEventListener.onRelayoutChildren(this);
}
if (indicatorAnimator != null && indicatorAnimator.isRunning()) {
// It's possible that the tabs' layout is modified while the indicator is animating (ex. a
// new tab is added, or a tab is removed in onTabSelected). This would change the target end
// position of the indicator, since the tab widths are different. We need to modify the
// animation's updateListener to pick up the new target positions.
updateOrRecreateIndicatorAnimation(
/* recreateAnimation= */ false, getSelectedTabPosition(), /* duration= */ -1);
} else {
// If we've been laid out, update the indicator position
jumpIndicatorToSelectedPosition();
}
}
依据进度改动TabView的巨细
private void tweenIndicatorPosition(View startTitle, View endTitle, float fraction) {
boolean hasVisibleTitle = startTitle != null && startTitle.getWidth() > 0;
if (hasVisibleTitle) {
if (tabEventListener != null && tabEventListener.matchMode(mode)) {
tabEventListener.onUpdateProgress(this, (TabView) startTitle, (TabView) endTitle, fraction);
}
tabIndicatorInterpolator.updateIndicatorForOffset(
TabLayout.this, startTitle, endTitle, fraction, tabSelectedIndicator);
} else {
// Hide the indicator by setting the drawable's width to 0 and off screen.
tabSelectedIndicator.setBounds(
-1, tabSelectedIndicator.getBounds().top, -1, tabSelectedIndicator.getBounds().bottom);
}
ViewCompat.postInvalidateOnAnimation(this);
}
在TabIndicatorInterpolator中刺进事情点
依据缩放值确认新鸿沟
static RectF calculateTabViewContentBounds(
@NonNull TabView tabView, @Dimension(unit = Dimension.DP) int minWidth) {
int tabViewContentWidth = tabView.getScaleContentWidth();
int tabViewContentHeight = tabView.getScaleContentHeight();
int minWidthPx = (int) ViewUtils.dpToPx(tabView.getContext(), minWidth);
if (tabViewContentWidth < minWidthPx) {
tabViewContentWidth = minWidthPx;
}
int tabViewCenterX = (tabView.getScaleLeft() + tabView.getScaleRight()) / 2;
int tabViewCenterY = (tabView.getScaleTop() + tabView.getScaleBottom()) / 2;
int contentLeftBounds = tabViewCenterX - (tabViewContentWidth / 2);
int contentTopBounds = tabViewCenterY - (tabViewContentHeight / 2);
int contentRightBounds = tabViewCenterX + (tabViewContentWidth / 2);
int contentBottomBounds = tabViewCenterY + (tabViewCenterX / 2);
return new RectF(contentLeftBounds, contentTopBounds, contentRightBounds, contentBottomBounds);
}
static RectF calculateIndicatorWidthForTab(TabLayout tabLayout, @Nullable View tab) {
if (tab == null) {
return new RectF();
}
// If the indicator should fit to the tab's content, calculate the content's width
if (!tabLayout.isTabIndicatorFullWidth() && tab instanceof TabView) {
return calculateTabViewContentBounds((TabView) tab, MIN_INDICATOR_WIDTH);
}
if (tab instanceof TabView) {
TabView tabView = (TabView) tab;
return new RectF(tabView.getScaleLeft(), tabView.getScaleTop(), tabView.getScaleRight(), tabView.getScaleBottom());
}
// Return the entire width of the tab
return new RectF(tab.getLeft(), tab.getTop(), tab.getRight(), tab.getBottom());
}
监听事情,对TabView进行自界说操控
tabEventListener = object : ITabEventListener {
private val selectedTabScale = 1.6f
private var startView: TabView? = null
private var endView: TabView? = null
private var maxScrollX = 0f
private var extraWidth = 0
override fun matchMode(mode: Int): Boolean {
return TabLayout.MODE_SCROLLABLE == mode
}
override fun onReMeasureChildren(slidingTabIndicator: LinearLayout, action: Consumer<Int>) {
slidingTabIndicator.clipChildren = false
slidingTabIndicator.clipToPadding = false
slidingTabIndicator.apply {
if (selectedTabScale == 1f) {
return
}
val largestTabWidth = children.fold(0) { acc, child ->
val tabView = child as TabView
if (child.visibility == View.VISIBLE) {
acc.coerceAtLeast(tabView.textView.measuredWidth)
} else {
acc
}
}
if (largestTabWidth <= 0) {
// If we don't have a largest child yet, skip until the next measure pass
return@apply
}
extraWidth = ((selectedTabScale - 1f) * largestTabWidth).roundToInt()
action.accept(measuredWidth + extraWidth)
}
}
override fun onRelayoutChildren(slidingTabIndicator: LinearLayout) {
layoutChildren(slidingTabIndicator, true)
}
override fun onTabViewLayout(tabView: TabLayout.TabView) {
tabView.clipChildren = false
tabView.clipToPadding = false
tabView.textView.apply {
pivotX = 0f
pivotY = height * 0.8f
}
}
override fun onUpdateProgress(slidingTabIndicator: LinearLayout, currentTab: TabView, nextTab: TabView?, progress: Float) {
if (startView != null && startView != currentTab && startView != nextTab) {
startView?.textView?.apply {
scaleX = 1f
scaleY = 1f
}
}
if (endView != null && endView != currentTab && endView != nextTab) {
endView?.textView?.apply {
scaleX = 1f
scaleY = 1f
}
}
currentTab.textView.apply {
val scale = selectedTabScale.plus(1.minus(selectedTabScale).times(progress))
scaleX = scale
scaleY = scale
startView = currentTab
}
nextTab?.textView?.apply {
val scale = 1.plus(selectedTabScale.minus(1f).times(progress))
scaleX = scale
scaleY = scale
endView = nextTab
}
layoutChildren(slidingTabIndicator, false)
}
private fun layoutChildren(slidingTabIndicator: LinearLayout, fromOnLayout: Boolean) {
var translationX = 0f
slidingTabIndicator.forEach { child ->
val tabView = child as TabView
if (child.visibility == View.VISIBLE) {
val rightGap = (tabView.textView.scaleX - 1).takeIf {
it != 0f
}?.let { (it * tabView.textView.measuredWidth) } ?: 0f
child.translationX = translationX
translationX += rightGap
}
}
maxScrollX = slidingTabIndicator.right - (slidingTabIndicator.parent as View).width - extraWidth + translationX
}
override fun getScaleTabContentWidth(tabView: TabView, originSize: Int): Int {
return tabView.textView.scaleX.times(originSize).toInt()
}
override fun getScaleTabContentHeight(tabView: TabView, originSize: Int): Int {
return tabView.textView.scaleY.times(originSize).toInt()
}
override fun getScaleTabWidth(tabView: TabView, originSize: Int): Int {
return originSize.plus(tabView.textView.scaleX.minus(1).times(tabView.textView.width).toInt())
}
override fun getScaleTabHeight(tabView: TabView, originSize: Int): Int {
return originSize.plus(tabView.textView.scaleY.minus(1).times(tabView.textView.height).toInt())
}
override fun transformScrollX(x: Int): Int {
return maxScrollX.toInt().coerceAtMost(x)
}
}
终究作用
![Demo](i.postimg.cc/0y0Ms5rJ/de…