系统应用闹钟之TabLayout的使用
此篇文章记录一些关于安卓原生应用闹钟里面TabLayout的使用,没见过用这个组件的源代码,想从它里面学习学习。就当是见见世面,顺便写个笔记。
先从印象到布局
原生闹钟给我的印象非常好,有种说不出的美感。在改工作上的问题时,顺便看它的样式,大致就是一个TabLayout加上四个fragment。
首先是头部的四个图标,是一个嵌在了Toolbar中的TabLayout。Toolbar也是一个ViewGroup,在它的里面放一个其他的控件也是理所当然,只是以前没试过。第一次看到这个写法,感觉非常好。
<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中。
<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感觉非常有意思,感觉它几乎无所不能,用到的任何东西都可以往里面取。
// 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也是固定的,同时这样写可以让那些信息更加集中,修改起来更加方便,让代码的逻辑性更强。看代码:
/** 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的问题,所以比较期待它是如何去实现的。
// 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,自己实现了其中的方法,具体如下:
/**
* 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;
}
}