AndroidTabLayout

AndroidTabLayout

2023年7月7日发(作者:)

AndroidTabLayout前⾔很久很久没写过源码解析了,不是⾃⼰没有看了,只是没有记录了,却发现不记录的话,似懂⾮懂,时间久了就忘得差不多了,⽤到了还是得再学⼀遍,忍住提笔⼀篇 TabLayout 源码学习。Hello World依赖添加 support design 包implementation 't:design:27.1.1'xml添加⼀个 TabLayout 就可以了

port out;import patActivity;import ;public class TabLayoutActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { te(savedInstanceState); setContentView(ty_tab_layout); TabLayout mTabLayout = findViewById(_layout); // 添加 tab item (().setText("TAB1")); (().setText("TAB2")); (().setText("TAB3")); (().setText("TAB4")); }}效果Screenshot_源码学习其实,实现这样⼀个布局并不难,让我们来看看⾥⾯所有的内容前世今⽣继承⾃ HorizontalScrollView 因为他⽀持滚动public class TabLayout extends HorizontalScrollView

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 sTabPool = new onizedPool<>(16);可滑动的指⽰条形图⾃定义 ViewGroup private class SlidingTabStrip extends LinearLayout

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 mTabLayoutRef; private int mPreviousScrollState; private int mScrollState; public TabLayoutOnPageChangeListener(TabLayout tabLayout) { mTabLayoutRef = new WeakReference<>(tabLayout); } @Override public void onPageScrollStateChanged(final int state) { mPreviousScrollState = mScrollState; mScrollState = state; } @Override public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) { final TabLayout tabLayout = (); if (tabLayout != null) { // Only update the text selection if we're not settling, or we are settling after // being dragged final boolean updateText = mScrollState != SCROLL_STATE_SETTLING || mPreviousScrollState == SCROLL_STATE_DRAGGING; // Update the indicator if we're not settling after being idle. This is caused // from a setCurrentItem() call and will be handled by an animation from // onPageSelected() instead. final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE); ollPosition(position, positionOffset, updateText, updateIndicator); } } @Override public void onPageSelected(final int position) { final TabLayout tabLayout = (); if (tabLayout != null && ectedTabPosition() != position && position < Count()) { // Select the tab, only updating the indicator if we're not being dragged/settled // (since onPageScrolled will handle that). final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE || (mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE); Tab(At(position), updateIndicator); } } void reset() { mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE; } }⼩结基本上看完了,但对于⼀些细节,滚动边界问题还没有深刻的理解,只知道⼤概的逻辑

发布者:admin,转转请注明出处:http://www.yc00.com/web/1688676565a161735.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信