实现APP高效导航

来源:互联网 发布:裙子太短 知乎 编辑:程序博客网 时间:2024/05/18 05:58

前言

今天看了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都添加监听器。

@Overridepublic 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()

@Overridepublic 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

@Overridepublic 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);}@Overridepublic 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就行啦。

@Overridepublic 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上连续进入很多网站,点击返回键的时候应该返回上一个网页,而不是直接结束网页浏览。

@Overridepublic void onBackPressed() {    if (mWebView.canGoBack()) {        mWebView.goBack();        return;    }    // Otherwise defer to system default behavior.    super.onBackPressed();}

如果会产生很多的历史记录,那么我们应该考虑周全,否则用户就觉得退出你的APP很费劲。


好了,今天的内容就这么多了。

0 0
原创粉丝点击