前言
今天看了Google教程上有关导航的知识,感觉只学到了一点。没有想象中的多。可能因为Google这个教程推出的时间比较早,所以当时的Android版本比较久,我打开官方示例项目的时候,有的API已经不推荐使用了。不过还是学到了一些,之前用到这么内容都是去网上搜,现在自己先真正学会一个简陋版复古版的导航,以后再来慢慢改善。
今天学到的内容:
- 将Tab和ViewPaper结合起来
- DrawerLayout的使用 侧边导航菜单
- 提供逻辑向上导航
- 提供合适的向后导航
- 实现后续导航
第一点和第二点侧重技术方面,后面三点都是更加侧重用户体验的细节处理
下面开始记录学习成果:
创建带有Tabs的滑动视图
结合ViewPaper和Tab的导航教程,实现相邻视图水平导航。
实现滑动视图
通过ViewPaper实现滑动视图,我们需要在XML中这么设置:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
我们知道ViewPaper管理的每一页都是独立,这时我们最好的选择是使用Fragment,因为它可以提高性能。如果用独立的View来显示,可能会很吃内存。
使用Fragment来显示每一页我们要提供相应的adapter,这时有两种选择:
FragmentPagerAdapter:如果是固定的,少量的页数推荐这个
FragmentStatePagerAdapter:数量不确定时选它,它会销毁过去的Fragemnt来最小化内存开销。
public class CollectionDemoActivity extends FragmentActivity {
// When requested, this adapter returns a DemoObjectFragment,
// representing an object in the collection.
DemoCollectionPagerAdapter mDemoCollectionPagerAdapter;
ViewPager mViewPager;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_collection_demo);
// ViewPager and its adapters use support library
// fragments, so use getSupportFragmentManager.
mDemoCollectionPagerAdapter =
new DemoCollectionPagerAdapter(
getSupportFragmentManager());
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mDemoCollectionPagerAdapter);
}
}
// Since this is an object collection, use a FragmentStatePagerAdapter,
// and NOT a FragmentPagerAdapter.
public class DemoCollectionPagerAdapter extends FragmentStatePagerAdapter {
public DemoCollectionPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int i) {
Fragment fragment = new DemoObjectFragment();
Bundle args = new Bundle();
// Our object is just an integer :-P
args.putInt(DemoObjectFragment.ARG_OBJECT, i + 1);
fragment.setArguments(args);
return fragment;
}
@Override
public int getCount() {
return 100;
}
@Override
public CharSequence getPageTitle(int position) {
return "OBJECT " + (position + 1);
}
}
// Instances of this class are fragments representing a single
// object in our collection.
public static class DemoObjectFragment extends Fragment {
public static final String ARG_OBJECT = "object";
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
// The last two arguments ensure LayoutParams are inflated
// properly.
View rootView = inflater.inflate(
R.layout.fragment_collection_object, container, false);
Bundle args = getArguments();
((TextView) rootView.findViewById(android.R.id.text1)).setText(
Integer.toString(args.getInt(ARG_OBJECT)));
return rootView;
}
}
上面演示了如何添加滑动视图,下面是如何在此基础上添加Tab导航。
在Action Bar上添加Tab
为了实现在Action Bar上添加Tab,你应该把Action Bar的导航模式设置成 NAVIGATION_MODE_TABS ,然后创建对应ViewPaper页数数量的ActionBar.Tab,然后给每一个Tab都添加监听器。
@Override
public void onCreate(Bundle savedInstanceState) {
final ActionBar actionBar = getActionBar();
...
// Specify that tabs should be displayed in the action bar.
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
// Create a tab listener that is called when the user changes tabs.
ActionBar.TabListener tabListener = new ActionBar.TabListener() {
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
// show the given tab
}
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
// hide the given tab
}
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
// probably ignore this event
}
};
// Add 3 tabs, specifying the tab's text and TabListener
for (int i = 0; i < 3; i++) {
actionBar.addTab(
actionBar.newTab()
.setText("Tab " + (i + 1))
.setTabListener(tabListener));
}
}
在添加完Tab之后,我们要把ViewPaper的滑动和Tab相关联。
将滑动视图和Tab相关联
首先先考虑第一种情况,点击Tab,然后滑动视图导航到对应的页。要实现这个目的,我们想想,因为触发的是Tab的监听器,所以我们应该在Tab的监听器的相应方法里面去设置ViewPaper刺客应该显示哪一页。监听类ActionBar.TabListener()
@Override
public void onCreate(Bundle savedInstanceState) {
...
// Create a tab listener that is called when the user changes tabs.
ActionBar.TabListener tabListener = new ActionBar.TabListener() {
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
// When the tab is selected, switch to the
// corresponding page in the ViewPager.
mViewPager.setCurrentItem(tab.getPosition());
}
...
};
}
同理,第二种情况:在滑动视图的时候改变Tab,滑动视图的时候触发ViewPaper的监听器,所以我们应该在ViewPaper的监听器的相应方法里改变Tab。监听类:ViewPager.OnPageChangeListener
@Override
public void onCreate(Bundle savedInstanceState) {
...
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setOnPageChangeListener(
new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
// When swiping between pages, select the
// corresponding tab.
getActionBar().setSelectedNavigationItem(position);
}
});
...
}
使用PagerTitleStrip而不是Tab
如果你不想用Tab导航,你可以用PagerTitleStrip来替换Tab,你只需要在XML文件里这么定义:
<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.PagerTitleStrip
android:id="@+id/pager_title_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="#33b5e5"
android:textColor="#fff"
android:paddingTop="4dp"
android:paddingBottom="4dp" />
</android.support.v4.view.ViewPager>
PagerTitleStrip是完全嵌套在ViewPager里面的。
不过这些样式都有些老了,如果要在APP中使用,我们要考虑用新一点的样式去替代它们。
创建一个导航Drawer
它是一个隐藏在主屏幕左边的APP导航选项,一般都是用ListView的形式。当用户手指从屏幕左侧滑动,或是点击Action Bar上的icon,它就会显示出来。
创建一个Drawer布局
在UI的根目录下定义DrawerLayout布局,这个布局包括两个View,一个View是用来显示屏幕里的内容,此时导航Drawer看不到,另一个View就是用来显示Drawer的导航内容的。
下面这个例子,通过FrameLayout来显示屏幕内容,用ListView 来显示导航内容:
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The main content view -->
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- The navigation drawer -->
<ListView android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:background="#111"/>
</android.support.v4.widget.DrawerLayout>
我们在写这个布局的时候应该要注意几点:
- 显示屏幕主要内容的View必须是第一个
- 显示屏幕主要内容的View应该充满父布局,因为它显示的时候Drawer要隐藏
- Drawer View应该使用android:layout_gravity来制定它的横向布局,为了更好的支持这个结构,值应该指定为start
- Drawer View的宽应该使用dp单位,高度匹配父布局。宽度不超过320dp,这样保证用户对屏幕内容始终可见
初始化Drawer列表
代码里的第一件事就应该是初始化Drawer列表,怎么做要看具体的APP要什么样的设计了。在这个例子里面,我们只要给ListView提供一个adapter就行了。
public class MainActivity extends Activity {
private String[] mPlanetTitles;
private DrawerLayout mDrawerLayout;
private ListView mDrawerList;
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPlanetTitles = getResources().getStringArray(R.array.planets_array);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerList = (ListView) findViewById(R.id.left_drawer);
// Set the adapter for the list view
mDrawerList.setAdapter(new ArrayAdapter<String>(this,
R.layout.drawer_list_item, mPlanetTitles));
// Set the list's click listener
mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
...
}
}
在Drawer列表中的每一项,我们都应该有相应的响应事件,下面会将如何实现Drawer点击事件。
处理导航点击事件
当用户点击列表的时候,系统会回调 OnItemClickListener 监听器里的 onItemClick() 方法,这个方法具体怎么写,就要看你要怎么实现APP了。
下面的代码演示点击列表然后插入对应的Fragment,在实际的项目中不要插入Fragment,而应先看有无Fragment实例,否则会徒增内存消耗:
private class DrawerItemClickListener implements ListView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectItem(position);
}
}
/** Swaps fragments in the main content view */
private void selectItem(int position) {
// Create a new fragment and specify the planet to show based on position
Fragment fragment = new PlanetFragment();
Bundle args = new Bundle();
args.putInt(PlanetFragment.ARG_PLANET_NUMBER, position);
fragment.setArguments(args);
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.content_frame, fragment)
.commit();
// Highlight the selected item, update the title, and close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mPlanetTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
@Override
public void setTitle(CharSequence title) {
mTitle = title;
getActionBar().setTitle(mTitle);
}
监听Drawer的打开和关闭事件
除了监听Drawer的打开和关闭事件,你应该给DrawerLayout设置监听器,监听器应该实现DrawerLayout.DrawerListener接口,在这个接口里面包括了监听Drawer打开和关闭的方法- onDrawerOpened() 和 onDrawerClosed()。
除了实现这个接口,如果使用了Action Bar,我们可以继承ActionBarDrawerToggle这个类,因为这个类继承了 DrawerLayout.DrawerListener 接口,所以我们直接覆写打开和关闭的方法就好了。直接使用ActionBarDrawerToggle还方便了我们后面对点击Action Bar的Icon实现Drawer的打开和关闭功能的实现。
我们应该监听什么呢?当Drawer打开的时候,我们应该把Action Bar的内容修改成APP的标题,或是其他的什么;当Drawer关闭的时候,我们应该把Action Bar的内容修改成和内容相关的标题。还可以设置Action item在打开和关闭时的可见性。
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
private CharSequence mDrawerTitle;
private CharSequence mTitle;
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
mTitle = mDrawerTitle = getTitle();
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getActionBar().setTitle(mTitle);
invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getActionBar().setTitle(mDrawerTitle);
invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
}
};
// Set the drawer toggle as the DrawerListener
mDrawerLayout.setDrawerListener(mDrawerToggle);
}
/* Called whenever we call invalidateOptionsMenu() */
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// If the nav drawer is open, hide action items related to the content view
boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
menu.findItem(R.id.action_websearch).setVisible(!drawerOpen);
return super.onPrepareOptionsMenu(menu);
}
}
通过APP图标来打开或关闭Drawer
用户可以用手指从左侧向右滑动来打开Drawer,如果使用的Action Bar,我们还可以实现点击Action Bar上的Icon来实现Drawer的开和关。通过之前提到的 ActionBarDrawerToggle 就能实现,代码几乎不用太大改变。
为了让ActionBarDrawerToggle工作,它需要五个参数
- 拥有Drawer的Activity
- DrawerLayout
- 用来指示打开关闭的drawable资源
- 用于描述打开Drawer的字符串
- 用于描述关闭Drawer的字符串
还有,要使用ActionBarDrawerToggle,我们必须在onPostCreate() 和 onConfigurationChanged()调用它:
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
...
public void onCreate(Bundle savedInstanceState) {
...
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(
this, /* host Activity */
mDrawerLayout, /* DrawerLayout object */
R.drawable.ic_drawer, /* nav drawer icon to replace 'Up' caret */
R.string.drawer_open, /* "open drawer" description */
R.string.drawer_close /* "close drawer" description */
) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getActionBar().setTitle(mTitle);
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getActionBar().setTitle(mDrawerTitle);
}
};
// Set the drawer toggle as the DrawerListener
mDrawerLayout.setDrawerListener(mDrawerToggle);
getActionBar().setDisplayHomeAsUpEnabled(true);
getActionBar().setHomeButtonEnabled(true);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
mDrawerToggle.syncState();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDrawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Pass the event to ActionBarDrawerToggle, if it returns
// true, then it has handled the app icon touch event
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
// Handle your other action bar items...
return super.onOptionsItemSelected(item);
}
...
}
提供向上返回按钮
这里的向上返回分为两种,一种我之前写过,是返回APP的逻辑上级,可以看这一篇博客,第二种是返回其他的APP(Activity可能由其他APP启动)。
向上返回逻辑Activity
请看这一篇博客
从新的返回堆栈中向上 导航
如果你的APP提供了 intent filters 允许其他APP启动的话,你就应该在 onOptionsItemSelected() 方法里考虑到这一种情况:如果Activity由其他APP启动,那当用户点击向上返回按钮的时候,你应该添加一个新的返回堆栈,防止用户一点击,你的APP就结束了。
你应该先调用 shouldUpRecreateTask()方法,检查Activity实例是都在其他APP的任务里,如果返回true,你就通过TaskStackBuilder新建一个任务堆栈。如果返回fasle,调用navigateUpFromSameTask() 返回最近的一个Parent Activity就行啦。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Respond to the action bar's Up/Home button
case android.R.id.home:
Intent upIntent = NavUtils.getParentActivityIntent(this);
if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
// This activity is NOT part of this app's task, so create a new task
// when navigating up, with a synthesized back stack.
TaskStackBuilder.create(this)
// Add all of this activity's parents to the back stack
.addNextIntentWithParentStack(upIntent)
// Navigate up to the closest parent
.startActivities();
} else {
// This activity is part of this app's task, so simply
// navigate up to the logical parent activity.
NavUtils.navigateUpTo(this, upIntent);
}
return true;
}
return super.onOptionsItemSelected(item);
}
注意:为了让addNextIntentWithParentStack()方法有效,你应该在manifest文件里声明每一个Activity的逻辑父Activity,通过android:parentActivityName (兼容更低版本的话就使用 < meta-data >,前面博客提到,不再累述)。
提供合适的向后导航
这里的内容比较琐碎分这几块:
- 从通知等途径直接开启你的APP
- 为Fragment提供向后导航
- 为WebView提供向后导航
第一点在上面就提到了,这里就不再说了,如果想看这一部分详细讲解的朋友,这一篇官方教程。
为Fragment提供向后导航
如果Fragment的模式是摘要和细节这样的模式的话,在替换Fragment的时候,之前的Fragment应该被加入返回堆栈中。比如说,在新闻阅读器里面,一个Fragment显示新闻标题,另一个Fragment显示新闻内容,点击对应的新闻标题后进入显示新闻内容的Fragment,为了在用户看完新闻后点击返回键不要直接返回手机桌面,我们应该把显示新闻标题的Fragment加入返回堆栈中,这样点击返回键后就会返回新闻标题的Fragment,而不会出现用户意料之外的退出了。我们通过调用 addToBackStack()来实现:
// Works with either the framework FragmentManager or the
// support package FragmentManager (getSupportFragmentManager).
getSupportFragmentManager().beginTransaction()
.add(detailFragment, "detail")
// Add this transaction to the back stack
.addToBackStack()
.commit();
如果你想修改其他的一些UI元素,来显示你现在Fragment的状态,你应该在加入返回堆栈之后更新UI。至于为什么我也不太懂,官方这么让我们去做的。通过添加 FragmentManager.OnBackStackChangedListener监听器,在加入堆栈后,改变UI。
getSupportFragmentManager().addOnBackStackChangedListener(
new FragmentManager.OnBackStackChangedListener() {
public void onBackStackChanged() {
// Update your UI here.
}
});
为webview实现向后导航
如果webview来访问网页,通常我们应该事先webview的向后导航,比如用户在webview上连续进入很多网站,点击返回键的时候应该返回上一个网页,而不是直接结束网页浏览。
@Override
public void onBackPressed() {
if (mWebView.canGoBack()) {
mWebView.goBack();
return;
}
// Otherwise defer to system default behavior.
super.onBackPressed();
}
如果会产生很多的历史记录,那么我们应该考虑周全,否则用户就觉得退出你的APP很费劲。
好了,今天的内容就这么多了。