尊重劳动成果,转载请注明出处:http://blog.csdn.net/growth58/article/details/48112793
关注新浪微博:@于卫国
邮箱:yuweiguocn@gmail.com
Google在2015 I/O大会上公布了Material Design Support Library,使用它可以创建materail应用在API 19以下突然变得很容易。在这个系列中,我们将使用RSS阅读器应用,被用于Material系列的基础应用,重写让它完全使用新的Design Support Library。
虽然我们将会重写之前的应用,仅仅是应用到UI层——我们将会重新使用RSS检索和解析代码(因此我不会讨厌在这里说明它)。然而,我们将要稍稍修改数据模型为了方便用于新的导航模型应用到这个app中。最基本的想法是Navigation Drawer将会包含一个文章列表——每篇文章都是单独的部分(已经被作为单独的博客文章发表)。当选择文章后,一个ViewPager将会用每个page设置在ViewPager中控制相应的部分。因此我们需要几个新类:
rss/model/Article.java
public class Article {
private final String title;
private final SparseArray<Item> parts;
public static Article newInstance(String title) {
SparseArray<Item> parts = new SparseArray<>();
return new Article(title, parts);
}
public Article(String title, SparseArray<Item> parts) {
this.title = title;
this.parts = parts;
}
public String getTitle() {
return title;
}
public void addPart(int partNumber, Item part) {
parts.put(partNumber, part);
}
public Item getPart(int partNumber) {
return parts.get(partNumber);
}
}
这个类代表每篇文章。注意一下SparseArray在这儿的使用。使用这个的原因是RSS订阅返回最后10篇被发表的文章,因此我们不能保证接收到所有的帖子在每篇文章中。通过使用SparseArray我们可以复制用一个文章集在第一文章还没开始时。
其它类表示文章AOU组,并负责从现有订阅对象中解析这些:
rss/model/Articles.java
public class Articles implements Iterable<String> {
private static final String TITLE_REGEX_STRING = "(.*)\\s+\\u2013\\s+Part\\s+([0-9]+)";
private static final Pattern TITLE_PATTERN = Pattern.compile(TITLE_REGEX_STRING);
private static final int TITLE_GROUP = 1;
private static final int PART_NUMBER_GROUP = 2;
private final Map<String, Article> articles;
private final List<String> titles;
public static Articles transform(Feed feed) {
Map<String, Article> articles = new HashMap<>();
List<String> titles = new ArrayList<>();
for (Item item : feed.getItems()) {
String title = item.getTitle();
Matcher matcher = TITLE_PATTERN.matcher(title);
if (matcher.find()) {
String baseTitle = matcher.group(TITLE_GROUP);
int part = Integer.parseInt(matcher.group(PART_NUMBER_GROUP));
Article article = articles.get(baseTitle);
if (article == null) {
article = Article.newInstance(baseTitle);
articles.put(baseTitle, article);
titles.add(baseTitle);
}
article.addPart(part, item);
} else {
Article article = Article.newInstance(title);
articles.put(title, article);
titles.add(title);
}
}
Collections.sort(titles);
return new Articles(titles, articles);
}
Articles(List<String> titles, Map<String, Article> articles) {
this.titles = titles;
this.articles = articles;
}
@Override
public Iterator<String> iterator() {
return titles.iterator();
}
public Article getArticle(String title) {
return articles.get(title);
}
public Article getFirst() {
return articles.get(titles.get(0));
}
}
基本的原则是每篇文章有一个标题形式“My Title-Part x”,然后我们使用一个正则表达式抽取一个基本的标题(如:例子中的“My Title”)和
part的数字(如:例子中的“x”),然后创建一个文章列表对象和一个有序的标题列表字符串可以在外面查询。
我已创建了一些标准的颜色和字符串资源,让我们快速查看一下我们应用的主题:
res/values/style.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Base.AppTheme" />
<style name="Base.AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/sa_green</item>
<item name="colorPrimaryDark">@color/sa_green_dark</item>
<item name="colorAccent">@color/sa_green</item>
<item name="colorControlHighlight">@color/sa_green_transparent</item>
</style>
</resources>
我们已经继承了一个标准的“NoActionBar” AppCompat主题,但我们将要在应用中使用一个Actionbar。选择这个主题的原因是我们将使用我们自己的工具条在布局文件中,因此我们不想使用系统给我们创建的ActionBar。值得注意的是我们声明了一个base主题被用来做为main主题的parent并且什么都没有重写。原因是我们需要添加一些额外的属性在Lollipop 及以后的设备上因此我们定义了一个共用的base:
res/values-v21/style.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Base.AppTheme">
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
这给了我们一个透明的状态条在Lollipop 及以后的设备。
下面让我们看一下我们的主activity布局文件:
res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:fitsSystemWindows="true">
<include layout="@layout/include_main" />
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header" />
</android.support.v4.widget.DrawerLayout>
这定义了一个DrawerLayout,包含NavigationView和主布局文件这两个,和NavigationView它自己。
对于在NavigationView 上指定菜单资源是可能的,可以用来显示一个静态导航菜单并且它是一个简单的也是有效的方式实现静态菜单。然而我们的菜单项取决于RSS订阅的内容,因此我们将需要通过编程实现在稍候。
NavigationView 也指定了一个headerLayout 将被用来作为一个header 在导航列表之上。通常这可以保存用户信息,但在我们这种情况下只是保存一个静态Styling Android标识,所有的内容来自Styling Android RSS订阅:
res/layout/nav_header.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/TextAppearance.AppCompat.Title.Inverse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:background="?attr/colorPrimary"
android:drawableLeft="@mipmap/ic_launcher"
android:gravity="center_vertical|start"
android:minHeight="100dp"
android:paddingLeft="8dp"
android:paddingTop="25dp"
android:text="@string/styling_android"
tools:ignore="Overdraw,RtlHardcoded,RtlSymmetry" />
被包含在activity 布局中的文件是include_main:
res/layout/include_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
</FrameLayout>
目前来说这是一个很好的例子——我们将稍候添加它。它包含我们的工具条用背景颜色设置为主要颜色从我们的主题中,应用一个适当的ActionBar 主题。
我们有几个类用来连接Activity和数据——DataFragment(已经在Material – Part 1深入讨论过)并且ArticlesConsumer 和Material 应用中的FeedConsumer是等效的。在这里我不讨厌提到这些。
剩下的事情就是重写我们的MainActivity:
MainActivity.java
public class MainActivity extends AppCompatActivity implements ArticlesConsumer {
private static final String DATA_FRAGMENT_TAG = DataFragment.class.getCanonicalName();
private static final int MENU_GROUP = 0;
private DrawerLayout drawerLayout;
private NavigationView navigationView;
private Articles articles;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
navigationView = (NavigationView) findViewById(R.id.nav_view);
setupToolbar();
setupNavigationView();
setupDataFragment();
}
这是相当直接的,我们展示出内容view,找到DrawerLayout的实例和NavigationView 对象,并且设置Toolbar,NavigationView 和DataFragment。让我们看看这些是如何设置的:
MainActivity.java
private void setupToolbar() {
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
找到Toolbar 的实例从布局文件中设置它作为支持的ActionBar,然后设置显示一个hamburger 菜单图标。
MainActivity.java
private void setupNavigationView() {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
menuItem.setChecked(true);
drawerLayout.closeDrawers();
selectArticle(menuItem.getTitle());
return true;
}
});
}
设置item 点击监听无论何时item被点击都将获得回调。当点击item时它将设置为checked,关闭drawer,然后触发一个改变在已选择的文章中(在这不久)。
MainActivity.java
private void setupDataFragment() {
DataFragment dataFragment = (DataFragment) getSupportFragmentManager().findFragmentByTag(DATA_FRAGMENT_TAG);
if (dataFragment == null) {
dataFragment = (DataFragment) Fragment.instantiate(this, DataFragment.class.getName());
dataFragment.setRetainInstance(true);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.add(dataFragment, DATA_FRAGMENT_TAG);
transaction.commit();
}
}
查看一下DataFragment 的实例在FragmentManager 中是否存在,如果不存在则创建一个新的实例(并存储)。
下一步我们要为hamburger 菜单按下添加一个handler (我们已经设置好了):
MainActivity.java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
drawerLayout.openDrawer(GravityCompat.START);
return true;
default:
//NO-OP
break;
}
return super.onOptionsItemSelected(item);
}
剩下的就是实现ArticlesConsumer 方法(activity实现这个接口)
MainActivity.java
@Override
public void setArticles(Articles articles) {
Menu menu = navigationView.getMenu();
menu.clear();
this.articles = articles;
int item = 0;
for (String articleTitle : articles) {
menu.add(MENU_GROUP, item, item, articleTitle);
if (item == 0) {
menu.getItem(0).setChecked(true);
}
item++;
}
menu.setGroupCheckable(MENU_GROUP, true, true);
setCurrentArticle(articles.getFirst());
}
@Override
public void handleError(String message) {
Snackbar.make(drawerLayout, message, Snackbar.LENGTH_LONG).show();
}
private void selectArticle(CharSequence title) {
Article article = articles.getArticle(title.toString());
setCurrentArticle(article);
}
private void setCurrentArticle(Article article) {
setTitle(article.getTitle());
}
当Articles 列表更新时,setArticles()将被调用,给NavigationView 构造新的菜单然后设置第一个作为默认的显示。
当发生错误的时候,handleError()将被调用,我们将构造一个SnackBar 显示错误。SnackBar 被构造方式和Toast基本相同,它需要用一个额外的View 和SnackBar 进行关联。正常情况下它将会作为CoordinatorLayout 的子view(我们将会在后面的系列中涵盖)目前我们仅仅只会使用它作为我们的顶级布局。
剩余两个方法设置当前的文章——对于现在我们仅仅修改了标题。
运行后可以看到效果:
这是当我们忘记添加INTERNET 权限到我们的清单文件中时,但证明了SnackBar 是正常工作的!
添加需要的权限看看完全使用Navigation Drawer 运行在API 21时的效果:
因此,我们的Navigation Drawer已经起作用了!在下一篇文章中我们将实现ViewPager 分别控制相应的文章部分。
源代码可以从这下载。
请我喝杯咖啡,请使用支付宝扫描下方二维码: