系统应用闹钟之TabLayout的使用

此篇文章记录一些关于安卓原生应用闹钟里面TabLayout的使用,没见过用这个组件的源代码,想从它里面学习学习。就当是见见世面,顺便写个笔记。

先从印象到布局

原生闹钟给我的印象非常好,有种说不出的美感。在改工作上的问题时,顺便看它的样式,大致就是一个TabLayout加上四个fragment

首先是头部的四个图标,是一个嵌在了Toolbar中的TabLayoutToolbar也是一个ViewGroup,在它的里面放一个其他的控件也是理所当然,只是以前没试过。第一次看到这个写法,感觉非常好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
app:elevation="0dp">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentInsetStart="0dp"
tools:ignore="RtlSymmetry">
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:tabGravity="fill"
app:tabIndicatorColor="@android:color/transparent"
app:tabMaxWidth="0dp"
app:tabMode="fixed"
app:tabPaddingEnd="0dp"
app:tabPaddingStart="0dp" />
</android.support.v7.widget.Toolbar>

然后在Toolbar下面就是一个ViewPager,存放在一个FrameLayout中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<android.support.v4.view.ViewPager
android:id="@+id/desk_clock_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:saveEnabled="false" />

<include layout="@layout/drop_shadow" />

</FrameLayout>

这样基本上大致的框架就出来了。接着看对它的初始化:
基本步骤和网上的教程是差不多的,只是其中的UiDataModel感觉非常有意思,感觉它几乎无所不能,用到的任何东西都可以往里面取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Create the tabs that make up the user interface.
mTabLayout = (TabLayout) findViewById(R.id.tabs);
final int tabCount = UiDataModel.getUiDataModel().getTabCount();
final boolean showTabLabel = getResources().getBoolean(R.bool.showTabLabel);
final boolean showTabHorizontally = getResources().getBoolean(R.bool.showTabHorizontally);
for (int i = 0; i < tabCount; i++) {
final UiDataModel.Tab tabModel = UiDataModel.getUiDataModel().getTab(i);
final @StringRes int labelResId = tabModel.getLabelResId();

final TabLayout.Tab tab = mTabLayout.newTab()
.setTag(tabModel)
.setIcon(tabModel.getIconResId())
.setContentDescription(labelResId);

if (showTabLabel) {
tab.setText(labelResId);
tab.setCustomView(R.layout.tab_item);

@SuppressWarnings("ConstantConditions")
final TextView text = (TextView) tab.getCustomView()
.findViewById(android.R.id.text1);
text.setTextColor(mTabLayout.getTabTextColors());

// Bind the icon to the TextView.
final Drawable icon = tab.getIcon();
if (showTabHorizontally) {
// Remove the icon so it doesn't affect the minimum TabLayout height.
tab.setIcon(null);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
} else {
text.setCompoundDrawablesRelativeWithIntrinsicBounds(null, icon, null, null);
}
}

打开UiDataModel,第一个引起我的关注的是一个枚举型,在其中保存了有关Tab的基本信息,如文本、图标资源id以及该Tab所对应的fragment等。第一次见到这种写法,有点震惊,同时觉得对于这种静态的应用,用枚举型去保存也符合常理,我觉得这种方法可以以后利用在我写的Android项目中,比如说我的应用中,Tab也是固定的,同时这样写可以让那些信息更加集中,修改起来更加方便,让代码的逻辑性更强。看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Identifies each of the primary tabs within the application. */
public enum Tab {
ALARMS(AlarmClockFragment.class, R.drawable.ic_tab_alarm, R.string.menu_alarm),
CLOCKS(ClockFragment.class, R.drawable.ic_tab_clock, R.string.menu_clock),
TIMERS(TimerFragment.class, R.drawable.ic_tab_timer, R.string.menu_timer),
STOPWATCH(StopwatchFragment.class, R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);

private final String mFragmentClassName;
private final @DrawableRes int mIconResId;
private final @StringRes int mLabelResId;

Tab(Class fragmentClass, @DrawableRes int iconResId, @StringRes int labelResId) {
mFragmentClassName = fragmentClass.getName();
mIconResId = iconResId;
mLabelResId = labelResId;
}

public String getFragmentClassName() { return mFragmentClassName; }
public @DrawableRes int getIconResId() { return mIconResId; }
public @StringRes int getLabelResId() { return mLabelResId; }
}

感觉自己简直被这源代码所陶醉。后面继续看关于TabLayout的初始化。这其中,对其的适配器的写法,我也是比较感兴趣的。因为自己在写应用的时候,遇到过关于PagerAdapter的问题,所以比较期待它是如何去实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Customize the view pager.
mFragmentTabPagerAdapter = new FragmentTabPagerAdapter(this);
mFragmentTabPager = (ViewPager) findViewById(R.id.desk_clock_pager);
// Keep all four tabs to minimize jank.
mFragmentTabPager.setOffscreenPageLimit(3);
// Set Accessibility Delegate to null so view pager doesn't intercept movements and
// prevent the fab from being selected.
mFragmentTabPager.setAccessibilityDelegate(null);
// Mirror changes made to the selected page of the view pager into UiDataModel.
mFragmentTabPager.addOnPageChangeListener(new PageChangeWatcher());
mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter);

// Mirror changes made to the selected tab into UiDataModel.
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
UiDataModel.getUiDataModel().setSelectedTab((UiDataModel.Tab) tab.getTag());
}

@Override
public void onTabUnselected(TabLayout.Tab tab) {
}

@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});

适配器的源代码不完全贴出来,感觉都对不起这源代码。它继承自ViewPager,自己实现了其中的方法,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* This adapter produces the DeskClockFragments that are the content of the DeskClock tabs. The
* adapter presents the tabs in LTR and RTL order depending on the text layout direction for the
* current locale. To prevent issues when switching between LTR and RTL, fragments are registered
* with the manager using position-independent tags, which is an important departure from
* FragmentPagerAdapter.
*/
final class FragmentTabPagerAdapter extends PagerAdapter {

private final DeskClock mDeskClock;

/** The manager into which fragments are added. */
private final FragmentManager mFragmentManager;

/** A fragment cache that can be accessed before {@link #instantiateItem} is called. */
private final Map<UiDataModel.Tab, DeskClockFragment> mFragmentCache;

/** The active fragment transaction if one exists. */
private FragmentTransaction mCurrentTransaction;

/** The current fragment displayed to the user. */
private Fragment mCurrentPrimaryItem;

FragmentTabPagerAdapter(DeskClock deskClock) {
mDeskClock = deskClock;
mFragmentCache = new ArrayMap<>(getCount());
mFragmentManager = deskClock.getFragmentManager();
}

@Override
public int getCount() {
return UiDataModel.getUiDataModel().getTabCount();
}

/**
* @param position the left-to-right index of the fragment to be returned
* @return the fragment displayed at the given {@code position}
*/
DeskClockFragment getDeskClockFragment(int position) {
// Fetch the tab the UiDataModel reports for the position.
final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);

// First check the local cache for the fragment.
DeskClockFragment fragment = mFragmentCache.get(tab);
if (fragment != null) {
return fragment;
}

// Next check the fragment manager; relevant when app is rebuilt after locale changes
// because this adapter will be new and mFragmentCache will be empty, but the fragment
// manager will retain the Fragments built on original application launch.
fragment = (DeskClockFragment) mFragmentManager.findFragmentByTag(tab.name());
if (fragment != null) {
fragment.setFabContainer(mDeskClock);
mFragmentCache.put(tab, fragment);
return fragment;
}

// Otherwise, build the fragment from scratch.
final String fragmentClassName = tab.getFragmentClassName();
fragment = (DeskClockFragment) Fragment.instantiate(mDeskClock, fragmentClassName);
fragment.setFabContainer(mDeskClock);
mFragmentCache.put(tab, fragment);
return fragment;
}

@Override
public void startUpdate(ViewGroup container) {
if (container.getId() == View.NO_ID) {
throw new IllegalStateException("ViewPager with adapter " + this + " has no id");
}
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}

// Use the fragment located in the fragment manager if one exists.
final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);
Fragment fragment = mFragmentManager.findFragmentByTag(tab.name());
if (fragment != null) {
mCurrentTransaction.attach(fragment);
} else {
fragment = getDeskClockFragment(position);
mCurrentTransaction.add(container.getId(), fragment, tab.name());
}

if (fragment != mCurrentPrimaryItem) {
FragmentCompat.setMenuVisibility(fragment, false);
FragmentCompat.setUserVisibleHint(fragment, false);
}

return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
final DeskClockFragment fragment = (DeskClockFragment) object;
fragment.setFabContainer(null);
mCurrentTransaction.detach(fragment);
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
final Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false);
FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false);
}
if (fragment != null) {
FragmentCompat.setMenuVisibility(fragment, true);
FragmentCompat.setUserVisibleHint(fragment, true);
}
mCurrentPrimaryItem = fragment;
}
}

@Override
public void finishUpdate(ViewGroup container) {
if (mCurrentTransaction != null) {
mCurrentTransaction.commitAllowingStateLoss();
mCurrentTransaction = null;
mFragmentManager.executePendingTransactions();
}
}

@Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment) object).getView() == view;
}
}