2023年7月7日发(作者:)
AndroidTabLayout前⾔很久很久没写过源码解析了,不是⾃⼰没有看了,只是没有记录了,却发现不记录的话,似懂⾮懂,时间久了就忘得差不多了,⽤到了还是得再学⼀遍,忍住提笔⼀篇 TabLayout 源码学习。Hello World依赖添加 support design 包implementation 't:design:27.1.1'xml添加⼀个 TabLayout 就可以了
TabLayout ⽀持两种模式,⼀种是固定的,⼀种是可滚动的(tab 太多,⼀屏显⽰不下,可使⽤这种模式,否则默认为平分) /** * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab * labels and a larger number of tabs. They are best used for browsing contexts in touch * interfaces when users don’t need to directly compare the tab labels. * * @see #setTabMode(int) * @see #getTabMode() */ public static final int MODE_SCROLLABLE = 0; /** * Fixed tabs display all tabs concurrently and are best used with content that benefits from * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. * Fixed tabs have equal width, based on the widest tab label. * * @see #setTabMode(int) * @see #getTabMode() */ public static final int MODE_FIXED = 1; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) @Retention() public @interface Mode {}Tab 的位置有两种,⼀种是居中,⼀种是平分 /** * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect * when used with {@link #MODE_FIXED}. * * @see #setTabGravity(int) * @see #getTabGravity() */ public static final int GRAVITY_FILL = 0; /** * Gravity used to lay out the tabs in the center of the {@link TabLayout}. * * @see #setTabGravity(int) * @see #getTabGravity() */ public static final int GRAVITY_CENTER = 1; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) @Retention() public @interface TabGravity {}居中模式屏幕快照 2019-02-21 下午创建 Tab使⽤代码创建 Tab public Tab newTab() { Tab tab = e(); if (tab == null) { tab = new Tab(); } t = this; = createTabView(tab); return tab; }Tab 还使⽤了 Pool,还是挺细⼼的 private static final
onMeasure如果设置了 MODE_FIXED 和 GRAVITY_CENTER 则需要重新测量,⽬的就是让居中,每个 ITEM 的宽度都是⼀样的,⽽且等于最⼤的⼀个,如果⼀屏放得下则需要重新设置每个 ITEM 的⼤⼩,并且重新测量。如果发不下,那么侧设置GRAVITY_FILL@Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { ure(widthMeasureSpec, heightMeasureSpec); if (e(widthMeasureSpec) != Y) { // HorizontalScrollView will first measure use with UNSPECIFIED, and then with // EXACTLY. Ignore the first call since anything we do will be overwritten anyway return; } // 重新测量 if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { final int count = getChildCount(); // First we'll find the widest tab int largestTabWidth = 0; for (int i = 0, z = count; i < z; i++) { View child = getChildAt(i); if (ibility() == VISIBLE) { largestTabWidth = (largestTabWidth, suredWidth()); } } if (largestTabWidth <= 0) { // If we don't have a largest child yet, skip until the next measure pass return; } // 间隔 final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); boolean remeasure = false; // ⼀屏放得下 if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { // If the tabs fit within our width minus gutters, we will set all tabs to have
// the same width for (int i = 0; i < count; i++) { final Params lp = (LayoutParams) getChildAt(i).getLayoutParams(); if ( != largestTabWidth || != 0) { = largestTabWidth; = 0; remeasure = true; } } } else { // If the tabs will wrap to be larger than the width minus gutters, we need // to switch to GRAVITY_FILL mTabGravity = GRAVITY_FILL; updateTabViews(false); remeasure = true; } if (remeasure) { // Now re-measure after our changes ure(widthMeasureSpec, heightMeasureSpec); } } }如何实现动画?通过移动 IndicatorViewonLayoutmIndicatorAnimator 是动画辅助类,在 onLayout 中,⾮空⽽且正在运⾏则看取消,然后调⽤ animateIndicatorToPosition,动画调⽤,否则直接设置位置,不⽀持动画 @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
ut(changed, l, t, r, b);
if (mIndicatorAnimator != null && ing()) {
// If we're currently running an animation, lets cancel it and start a
// new animation with the remaining duration
();
final long duration = ation();
animateIndicatorToPosition(mSelectedPosition,
((1f - matedFraction()) * duration)); } else {
// If we've been layed out, update the indicator position
updateIndicatorPosition();
}
}
updateIndicatorPosition ⾸先获取选中的 View,然后看 mSelectionOffset 是否⼤于零,说明发⽣滚动,则需要重新计算新位置 private void updateIndicatorPosition() {
final View selectedTitle = getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && th() > 0) {
left = t();
right = ht();
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { // Draw the selection partway between the tabs
View nextTitle = getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * t() +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * ht() +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
// mIndicatorLeft 和 mIndicatorRight 控制了线的起始位置 void setIndicatorPosition(int left, int right) {
if (left != mIndicatorLeft || right != mIndicatorRight) {
// If the indicator's left/right has changed, invalidate mIndicatorLeft = left;
mIndicatorRight = right;
validateOnAnimation(this);
}
}
移动动画,移动间隔⼤的话,并不会从当前位置直接移动,⽽是跳跃⼀段距离再移动,通过 startLeft 和 startRight 控制,并且使⽤ValueAnimator 来实现动画这个不错,同意了 fraction 0-1 ,通过函数计算进度setIndicatorPosition(
(startLeft, targetLeft, fraction),
(startRight, targetRight, fraction)); void animateIndicatorToPosition(final int position, int duration) {
if (mIndicatorAnimator != null && ing()) {
();
}
final boolean isRtl = outDirection(this)
== _DIRECTION_RTL;
final View targetView = getChildAt(position);
if (targetView == null) {
// If we don't have a view, just update the position now and return
updateIndicatorPosition();
return;
}
final int targetLeft = t();
final int targetRight = ht();
final int startLeft;
final int startRight;
if ((position - mSelectedPosition) <= 1) {
// If the views are adjacent, we'll animate from edge-to-edge
startLeft = mIndicatorLeft;
startRight = mIndicatorRight;
} else {
// Else, we'll just grow from the nearest edge
final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
if (position < mSelectedPosition) {
// We're going end-to-start
if (isRtl) {
startLeft = startRight = targetLeft - offset;
} else {
startLeft = startRight = targetRight + offset;
}
} else {
// We're going start-to-end
if (isRtl) {
startLeft = startRight = targetRight + offset;
} else {
startLeft = startRight = targetLeft - offset;
}
}
}
// 开始移动位置
if (startLeft != targetLeft || startRight != targetRight) {
ValueAnimator animator = mIndicatorAnimator = new ValueAnimator();
erpolator(_OUT_SLOW_IN_INTERPOLATOR);
ation(duration);
atValues(0, 1);
ateListener(new orUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
final float fraction = matedFraction();
setIndicatorPosition(
(startLeft, targetLeft, fraction),
(startRight, targetRight, fraction));
}
});
tener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
mSelectedPosition = position;
mSelectionOffset = 0f;
}
});
();
}
}
onDraw 很简单 @Override
public void draw(Canvas canvas) {
(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
ct(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
TabView接下来再看看上⾯的内容 class TabView extends LinearLayout { private Tab mTab;
private TextView mTextView;
private ImageView mIconView;
private View mCustomView;
private TextView mCustomTextView;
private ImageView mCustomIconView;
private int mDefaultMaxLines = 2;
}如何实现监听的,对每个 TabView 设置点击事件,重写了 performClick,其中调⽤ ,mTab 拥有 TabLayout 的引⽤, @Override
public boolean performClick() {
final boolean handled = mClick();
if (mTab != null) {
if (!handled) {
playSoundEffect(); }
();
return true;
} else {
return handled;
}
}
/**
* Select this tab. Only valid if the tab has been added to the action bar.
*/
public void select() {
if (mParent == null) {
throw new IllegalArgumentException("Tab not attached to a TabLayout");
}
Tab(this);
}
tabLayout 回调事件, void selectTab(Tab tab) {
selectTab(tab, true);
}
void selectTab(final Tab tab, boolean updateIndicator) {
final Tab currentTab = mSelectedTab;
if (currentTab == tab) {
if (currentTab != null) {
dispatchTabReselected(tab);
animateToTab(ition());
}
} else {
final int newPosition = tab != null ? ition() : D_POSITION; if (updateIndicator) {
if ((currentTab == null || ition() == D_POSITION && newPosition != D_POSITION) {
// If we don't currently have a tab, just draw the indicator
setScrollPosition(newPosition, 0f, true);
} else {
animateToTab(newPosition);
}
if (newPosition != D_POSITION) {
setSelectedTabView(newPosition);
}
}
if (currentTab != null) {
dispatchTabUnselected(currentTab);
}
mSelectedTab = tab;
if (tab != null) {
dispatchTabSelected(tab);
}
}
}
移动 Tab,动画 private void animateToTab(int newPosition) {
if (newPosition == D_POSITION) {
return;
}
if (getWindowToken() == null || !Out(this)
|| enNeedLayout()) {
// If we don't have a window token, or we haven't been laid out yet just dra // position now
setScrollPosition(newPosition, 0f, true);
return;
}
final int startScrollX = getScrollX();
final int targetScrollX = calculateScrollXForTab(newPosition, 0);
if (startScrollX != targetScrollX) {
ensureScrollAnimator();
Values(startScrollX, targetScrollX);
();
}
// Now animate the indicator
eIndicatorToPosition(newPosition, ANIMATION_DURATION);
}
计算移动的距离,让选中的 tab 位于中间位置,由于 Android ScrollView 默认不会滚动超出边界,所以如果到达边界也不会继续滚动了 private int calculateScrollXForTab(int position, float positionOffset) {
if (mMode == MODE_SCROLLABLE) {
final View selectedChild = ldAt(position);
final View nextChild = position + 1 < ldCount()
ldAt(position + 1)
: null;
final int selectedWidth = selectedChild != null ? th() : 0;
final int nextWidth = nextChild != null ? th() : 0;
// base scroll amount: places center of tab in center of parent
int scrollBase = t() + (selectedWidth / 2) - (getWidth() / 2);
// offset amount: fraction of the distance between centers of tabs
int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
return (outDirection(this) == _DIRECTION_LTR)
scrollBase + scrollOffset
: scrollBase - scrollOffset;
}
return 0;
}
ViewPager其实很简单,就是给 ViewPager 添加⼀个 OnPageChangeListener 就⾏了,代码也很简单,在 onPageScrolled 中改变 指⽰条 的位置,在onPageSelected 中改变 选中状态 public static class TabLayoutOnPageChangeListener implements ChangeListener { private final WeakReference
发布者:admin,转转请注明出处:http://www.yc00.com/web/1688676565a161735.html
评论列表(0条)