Material Design是在2014年Google I/O大会上重磅推出的一套全新的界面设计语言,是由谷歌的设计工程师们基于传统优秀的设计原则,结合丰富的创意和科学技术所发明的一套全新的界面设计语言,包含了视觉、运动、互动效果等特性。
为支持Material Design UI设计风格,谷歌在2015年的Google I/O大会上推出了一个Design Support库,这个库将Material Design中最具代表性的一些控件和效果进行了封装,使得开发者在即使不了解Material Design的情况下也能非常轻松地将自己地应用Material化。
一、Toolbar
Toolbar将会是我们接触地第一个Material控件,虽说对于Toolbar你暂时应该还是比较陌生地,但是对于它地另一个相关控件ActionBar,你应该有点熟悉。每个活动最顶部地那个标题栏其实就i是ActionBar,不过ActionBar由于其设计地原因,被限定只能位于活动地顶部,从而不能实现一些Material Design的效果,因此官方现在已经不再建议使用ActionBar了。
Toolbar的强大之处在于,它不仅继承了ActionBar的所有功能,而且灵活性很高,可以配合其他控件来完成一些Material Design的效果。
首先你要知道,任何一个新建的项目,默认都是会显示ActionBar的,那么这个ActionBar到底是哪里来的呢?其实这是根据项目中指定的主题来显示的,AndroidManifest.xml中如下所示:
android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportRtl="true" android:theme=‘@style/AppTheme" > ...
可以看到,这里使用android:theme属性指定了一个AppTheme的主题,那么这个AppTheme又是再哪里定义的呢?打开res/values/styles.xml文件,代码如下:
这里定义了一个叫AppTheme的主题,然后指定它的parent主题是Theme.AppCompat.Light.DarkActionBar。这个DarkActionBar是一个深色的ActionBar主题,我们之前所有的项目中自带的ActionBar就是因为指定了这个主题才出现的。
而现在我们准备使用Toolbar来替代ActionBar,因此需要指定一个不带ActionBar的主题,通常Theme.AppCompat.NoActionBar和ThemeAppCompat.Light.NoActionBar这两种主题可选,其中Theme.AppCompat.NoActionBar表示深色主题,它会将界面的主体颜色设成深色,陪衬颜色设成淡色。而Theme.AppCompat.Light.NoActionBar表示淡色主题,他会将界面的主体颜色设成淡色,陪衬颜色设成深色。具体效果你可以试一试,这里由于我们之前的程序一直都是以淡色为主的,那么我就选用淡色主题了了,如下所示:
然后观察一下AppTheme中的属性重写,这里重写了colorPrimary、colorPrimaryDark和colorAccent这3个属性的颜色。除上述3个属性之外,我们还可以通过textColorPrimary、windowBackground和navigationBarColor等属性来控制更多位置的颜色。不过唯独colorAccent这个属性比较难理解,它不只是用来指定这样一个按钮的颜色,而是更多表达了一个强调的意思,比如一些控件的选中状态也会使用这个颜色。
现在我们已经将ActionBar隐藏起来了,那么接下来看一看如何使用Toolbar来替代ActionBar。修改activity_material_design_test.xml中的代码,如下所示:
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width=‘match_parent" android:layout_height="match_parent"> 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" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
toolbar.xml代码如下:
xml version="1.0" encoding="utf-8"?>
使用app:showAsAction来指定按钮的显示位置,之所以这里再次使用了app命名空间,同样是为了能够兼容低版本的系统。showAsAction主要有以下几种值可选:always表示永远显示在Toolbar中,如果屏幕控件不够则不显示;ifRoom表示屏幕控件足够的情况下显示在Toolbar中,不够的化就显示在菜单当中;never则表示永远显示在菜单当中。注意,Toolbar中的action按钮只会显示图标,菜单中的action按钮只会显示文字。
主代码如下:
public class MaterialDesignTestActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_material_design_test); initUI(); } public void initUI(){ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); if(actionBar!=null){ actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.mipmap.ic_launcher); } } public boolean onCreateOptionsMenu(Menu menu){ getMenuInflater().inflate(R.menu.toolbar,menu); return true; } public boolean onOptionsItemSelected(MenuItem item){ switch(item.getItemId()){ case R.id.backup: Toast.makeText(this,"You clicked Backup",Toast.LENGTH_SHORT).show(); break; case R.id.delete: Toast.makeText(this,"You clicked Delete",Toast.LENGTH_SHORT).show(); break; case R.id.settings: Toast.makeText(this,"You clicked Settings",Toast.LENGTH_SHORT).show(); break; default: } return true; } }
二、滑动菜单
滑动菜单可以说是Material Design中最常见的效果之一了。所谓的滑动菜单就是将一些菜单选项隐藏起来,而不是放置在主屏幕上,然后通过滑动的方式将菜单显示出来。这种方式既节省了屏幕空间,又实现了非常好的动画效果,是Material Design中推荐的做法。
1.DrawerLayout
activity_material_design_test.xml中代码如下:
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_width="match_parent" android:layout_height="match_parent" > android:layout_width="match_parent" android:layout_height="match_parent" > 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" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> android:layout_width=‘match_parent" android:layout_height="match_parent" android:layout_gravity="start" android:text="This is menu" android:textSize="30sp" android:background="#FFF" />
可以看到,这里最外层的控件使用了DrawerLayout,这个控件是由support-v4库提供的。DrawerLayout中放置了两个直接子控件,第一个子空间是FrameLayout,用于作为主屏幕显示的内容;第二个子控件这里使用了一个TextView,用于作为滑动菜单中显示的内容,起始使用什么都可以,DrawerLayout并没有限制只能使用固定的控件。
但是关于第二个子控件有一点需要注意,layout_gravity这个属性是必须指定的,因为我们需要告诉DrawerLayout滑动菜单是在屏幕的左边还是右边,指定left表示滑动菜单在左边,指定right滑动菜单在右边,这里指定start,表示会根据系统语言进行判断。
public class MaterialDesignTestActivity extends AppCompatActivity { private DrawerLayout mDrawerLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_material_design_test); initUI(); } public void initUI(){ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawaer_layout); ActionBar actionBar = getSupportActionBar(); if(actionBar!=null){ actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.mipmap.ic_launcher); } /*findViewById(R.id.tv_menu).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mDrawerLayout.closeDrawers(); } });*/ } public boolean onCreateOptionsMenu(Menu menu){ getMenuInflater().inflate(R.menu.toolbar,menu); return true; } public boolean onOptionsItemSelected(MenuItem item){ switch(item.getItemId()){ case android.R.id.home: mDrawerLayout.openDrawer(GravityCompat.START); break; case R.id.backup: Toast.makeText(this,"You clicked Backup",Toast.LENGTH_SHORT).show(); break; case R.id.delete: Toast.makeText(this,"You clicked Delete",Toast.LENGTH_SHORT).show(); break; case R.id.settings: Toast.makeText(this,"You clicked Settings",Toast.LENGTH_SHORT).show(); break; default: } return true; } }
2.NavigationView
NavigationView是Design Support库中提供的一个控件,它不仅是严格按照MaterialDesign的要求来进行设计的,而且还可以将滑动菜单页面的实现变得非常简单。
首先,这个控件是Design Support库中提供的,俺么我们就需要将这个库引入到项目中才行。打开app/build.gradle文件,再dependencies闭包中添加如下内容:
dependencies{
compile fileTree(dir:'libs',include:['*.jar'])
compile 'com.android.support.appcompat-v7:24.2.1'
testCompile 'junit:junit:4.12'
compile 'com.android.support;design;24.2.1'
compile 'de.hdodenhof:circleimageview:2.1.0'
}
这里添加了两行依赖关系,第一行就是Design Support库,第二行是一个开源项目CircleImageView,它可以用来轻松实现图片图形化的功能,我们待会就会用到它。CircleImageView的项目主页地址是:https://github.com/hdodenhof/CircleImageView。
在开始使用之前,我们还需要提前准备好两个东西:menu和headerLayout。menu是用来在NavigationView中显示具体的菜单项的,headerLayout则是用来在NavigationView中显示头部布局的。
nav_menu.xml代码如下:
xml version="1.0" encoding="utf-8"?>
nav_header.xml代码如下:
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="180dp" android:padding="10dp" android:background="?attr/colorPrimary" > android:id="@+id/icon_iamge" android:layout_width="70dp" android:layout_height="70dp" android:src="@mipmap/ic_launcher" android:layout_centerInParent="true" /> android:id="@+id/mail" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:text="[email protected]" android:textColor="#FFF" android:textSize="14sp" /> android:id="@+id/username" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@id/mail" android:text="Tony Green" android:textColor="#FFF" android:textSize="14sp" />
修改activity_main.xml代码,如下所示:
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/drawaer_layout" > android:layout_width="match_parent" android:layout_height="match_parent" > 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" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> android:id="@+id/nav_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" app:menu="@menu/nav_menu" app:headerLayout="@layout/nav_header" />
这里通过app:menu和app:headerLayout属性将我们刚才准备好的menu和headerLayout设置了进去,这样NavigationView就定义完成了。
public class MaterialDesignTestActivity extends AppCompatActivity { private DrawerLayout mDrawerLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_material_design_test); initUI(); } public void initUI(){ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawaer_layout); NavigationView navView = (NavigationView) findViewById(R.id.nav_view); ActionBar actionBar = getSupportActionBar(); if(actionBar!=null){ actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.mipmap.ic_launcher); } navView.setCheckedItem(R.id.nav_call); navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener(){ @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { mDrawerLayout.closeDrawers(); return true; } }); /*findViewById(R.id.tv_menu).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mDrawerLayout.closeDrawers(); } });*/ } public boolean onCreateOptionsMenu(Menu menu){ getMenuInflater().inflate(R.menu.toolbar,menu); return true; } public boolean onOptionsItemSelected(MenuItem item){ switch(item.getItemId()){ case android.R.id.home: mDrawerLayout.openDrawer(GravityCompat.START); break; case R.id.backup: Toast.makeText(this,"You clicked Backup",Toast.LENGTH_SHORT).show(); break; case R.id.delete: Toast.makeText(this,"You clicked Delete",Toast.LENGTH_SHORT).show(); break; case R.id.settings: Toast.makeText(this,"You clicked Settings",Toast.LENGTH_SHORT).show(); break; default: } return true; } }
三、悬浮按钮和可交互提示
1.FloatingActionButton
FloatingActionButton是Design Support库中提供的一个控件,这个控件可以帮助我们比较轻松地实现悬浮按钮地效果。
布局代码如下所示:
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/drawaer_layout" > android:layout_width="match_parent" android:layout_height="match_parent" > 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" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@android:drawable/ic_menu_share" app:elevation="8dp" />
这里使用app:elevation属性来给FloatingActionButton指定一个高度值,高度值越大,投影范围也越大,但是投影效果越淡,高度值越小,投影范围也越小,但是投影效果越浓。
代码中FloatingActionButton点击事件处理如下:
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view) { Toast.makeText(MaterialDesignTestActivity.this,"FAB clicked",Toast.LENGTH_SHORT).show(); } });
2.Snackbar
提示工具Snackbar并不是Toast的替代品,它们两者之间有着不同的应用场景。Toast的作用是告诉用户现在发生了什么事情,但同时用户只能被动接收这个事情,因为没有什么办法让用户进行选择。而Snackbar则在这方面进行了扩展,它允许在提示当中加入一个可交互按钮,当用户点击按钮的时候可以执行一些额外的逻辑操作。打个比方,如果我们在执行删除操作的时候只弹出一个Toast提示,那么用户要是误删了后果很严重,但是如果我们增加一个Undo按钮,就相当于给用户提供了一种弥补措施,从而大大降低了事故发生的概率,提升了用户体验。
Snackbar的用法也非常简单,它和Toast是基本相似的,只不过可以额外增加一个按钮的点击事件。
代码中实现如下所示:
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view) { Snackbar.make(view,"Data delete", Snackbar.LENGTH_SHORT). setAction("Undo",new View.OnClickListener(){ @Override public void onClick(View view) { Toast.makeText(MaterialDesignTestActivity.this,"Data restored",Toast.LENGTH_SHORT).show(); } }).show(); } });
Snackbar的make()方法来创建一个Snackbar对象,make()方法的第一个参数需要传入一个View,只要是当前姐买你布局的任意一个View都可以,Snackbar会使用这个View来自动查找最外层的布局,用于展示Snackbar。第二个参数就是Snackbar中显示的内容,第三个参数是Snackbar显示的时长。接着又调用了一个setAction()方法来设置一个动作,从而让Snackbar不仅仅是一个提示,而是可以和用户进行交互的。最后调用show()方法让Snackbar显示出来。
不管是出现还是消失,Snackbar都是带有动画小伙的,因此视觉体验也会比较好。
不过你有没有发现一个bug,这个Snackbar竟然将我们的悬浮按钮给遮挡住了。虽然说也不是什么重大问题,因为Snackbar过一会儿就会自动消失,但这种用户体验总归是不友好的。有没有什么办法能解决一下呢?当然有,只需要借助CoordinatorLayout就可以轻松解决。
3.CoordinatorLayout
CoordinatorLayout可以说是一个加强版的FrameLayout,这个库也是由Design Support库提供的。它在普通情况下的作用和FrameLayout基本一致。事实上,CoordinatorLayout可以监听其所有子控件的各种事件,然后自动帮助我们做出最为合理的响应。
举个简单的例子,刚才弹出的Snackbar提示将悬浮按钮遮挡住了,而如果我们能让CoordinatorLayout监听到Snackbar的弹出事件,那么它会自动将内部的FloatingActionButton向上偏移,从而确保不会被Snackbar遮挡到。
将原布局中的FrameLayout替换成CoordinatorLayout,代码如下:
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/drawaer_layout" > android:layout_width="match_parent" android:layout_height="match_parent" > 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" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@android:drawable/ic_menu_share" app:elevation="8dp" /> android:id="@+id/nav_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" app:menu="@menu/nav_menu" app:headerLayout="@layout/nav_header" />
由于CoordinatorLayout本身就是一个加强版的FrameLayout,因此这种替换不会有任何的副作用。
可以看到,悬浮按钮自动向上偏移了Snackbar的同等高度,从而确保不会被遮挡住,当Snackbar消失的时候,悬浮按钮会自动向下偏移回到原来位置。
另外悬浮按钮的向上和向下偏移也是伴随着动画效果的,且和Snackbar完全同步,整体效果看上去特别赏心悦目。
不过我们回过头来再思考一下,刚才说的是CoordinatorLayout可以监听其所有子控件的各种事件,但是Snackbar好像并不是CoordiantorLayout的子控件吧,为什么它却可以被监听到呢?
其实道理很简单,还记得我们再Snackbar的make()方法中传入的第一个参数吗?这个参数就是用来指定Snackbar是基于哪个View来出发的,刚才我们传入的是FloatingActionButton本身,而其是CoordinatorLayout中的子控件,因此这个事件就理所应当能被监听到了。你可以做个试验,如果给Snackbar的make()方法出入一个DrawerLayout,那么Snackbar就会再次遮挡住悬浮按钮,因为DrawerLayout不是CoordinatorLayout的子控件,CoordinatorLayout也就无法监听到Snackbar的弹出和隐藏事件了。
四、卡片式布局
卡片式布局也是Materials Design中提出的一个新概念,它可以让页面中的元素看起来就像是再卡片中一样,并且还鞥你拥有圆角和投影。
1.CardView
CardView是用于实现卡片式布局效果的重要控件,由appcompat-v7库提供。实际上,CardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉。
eg.
android:layout_width=’match_parent" android:layout_height="wrap_content" app:cardCornerRadius=”4dp“ app:elevation="5dp" > android:id="@+id.info_text" android:layout_width="match_parent" android:layout_height="wrap_content" />
这里定义了一个CardView布局,我们可以通过app;cardCornerRadius属性指定卡片圆角的弧度,数值远大,圆角的弧度也越大。另外还可以通过app:elevation属性指定卡片的高度,高度值越大,投影范围也越大,但是投影效果越淡,高度值越小,投影范围也越小,但是投影效果越浓。
添加波纹点击效果
默认情况,CardView是不可点击的,并且没有任何的触摸反馈效果。触摸反馈动画在用户点击CardView时可以给用户以视觉上的反馈。为了实现这种行为,你必须提供一下属性:
...
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground">
...
使用android:foreground=”?android:attr/selectableItemBackground”可以使CardView点击产生波纹的效果,有触摸点向外扩散。
常见属性
eg.实现图片列表
依赖库:
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' testCompile 'junit:junit:4.12' compile 'com.android.support:design:25.3.1' compile 'com.github.bumptech.glide:glide:3.7.0' compile 'com.android.support:recyclerview-v7:25.3.1' compile 'de.hdodenhof:circleimageview:2.1.0' compile 'com.android.support:cardview-v7:25.3.1' }
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/drawaer_layout" > android:layout_width="match_parent" android:layout_height="match_parent" > 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" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" /> android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@android:drawable/ic_menu_share" app:elevation="8dp" /> android:id="@+id/nav_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" app:menu="@menu/nav_menu" app:headerLayout="@layout/nav_header" />
public class Fruit { private String name; private int imageId; public Fruit(String name, int imageId) { this.name = name; this.imageId = imageId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getImageId() { return imageId; } public void setImageId(int imageId) { this.imageId = imageId; } }
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="5dp" app:cardCornerRadius="4dp" > android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > android:id="@+id/fruit_iamge" android:layout_width="match_parent" android:layout_height="100dp" android:scaleType="centerCrop" /> android:id="@+id/fruit_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_margin="5dp" android:textSize="16sp" />
为RecyclerView准备一个适配器,新建FruitAdapter类,代码如下所示:
public class FruitAdapter extends RecyclerView.Adapter{ private Context mContext; private List mFruitList; static class ViewHolder extends RecyclerView.ViewHolder{ CardView cardView; ImageView fruitImage; TextView fruitName; public ViewHolder(View view){ super(view); cardView = (CardView) view; fruitImage = (ImageView) view.findViewById(R.id.fruit_iamge); fruitName = (TextView) view.findViewById(R.id.fruit_name); } } public FruitAdapter(List fruitList){ mFruitList=fruitList; } public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType){ if(mContext==null){ mContext=parent.getContext(); } View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item,parent,false); return new ViewHolder(view); } @Override public void onBindViewHolder(ViewHolder holder, int position) { Fruit fruit = mFruitList.get(position); holder.fruitName.setText(fruit.getName()); Glide.with(mContext).load(fruit.getImageId()).into(holder.fruitImage); } public int getItemCount(){ return mFruitList.size(); } }
主代码如下所示:
public class MaterialDesignTestActivity extends AppCompatActivity { private DrawerLayout mDrawerLayout; private Fruit[] fruits = {new Fruit("Apple",R.mipmap.ic_launcher),new Fruit("Banana",R.mipmap.ic_launcher), new Fruit("Orange",R.mipmap.ic_launcher),new Fruit("Watermelon",R.mipmap.ic_launcher), new Fruit("Pear",R.mipmap.ic_launcher),new Fruit("Grape",R.mipmap.ic_launcher), new Fruit("Pineapple",R.mipmap.ic_launcher),new Fruit("Strawberry",R.mipmap.ic_launcher), new Fruit("Cherry",R.mipmap.ic_launcher),new Fruit("Mango",R.mipmap.ic_launcher) }; private ListfruitList = new ArrayList<>(); private FruitAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_material_design_test); initUI(); initFruits(); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); GridLayoutManager layoutManager = new GridLayoutManager(this,2); recyclerView.setLayoutManager(layoutManager); adapter = new FruitAdapter(fruitList); recyclerView.setAdapter(adapter); } public void initUI(){ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawaer_layout); NavigationView navView = (NavigationView) findViewById(R.id.nav_view); ActionBar actionBar = getSupportActionBar(); if(actionBar!=null){ actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.mipmap.ic_launcher); } navView.setCheckedItem(R.id.nav_call); navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener(){ @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { mDrawerLayout.closeDrawers(); return true; } }); /*findViewById(R.id.tv_menu).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mDrawerLayout.closeDrawers(); } });*/ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view) { Snackbar.make(view,"Data delete", Snackbar.LENGTH_SHORT). setAction("Undo",new View.OnClickListener(){ @Override public void onClick(View view) { Toast.makeText(MaterialDesignTestActivity.this,"Data restored",Toast.LENGTH_SHORT).show(); } }).show(); } }); } private void initFruits(){ fruitList.clear(); for(int i=0;i<50;i++){ Random random = new Random(); int index = random.nextInt(fruits.length); fruitList.add(fruits[index]); } } public boolean onCreateOptionsMenu(Menu menu){ getMenuInflater().inflate(R.menu.toolbar,menu); return true; } public boolean onOptionsItemSelected(MenuItem item){ switch(item.getItemId()){ case android.R.id.home: mDrawerLayout.openDrawer(GravityCompat.START); break; case R.id.backup: Toast.makeText(this,"You clicked Backup",Toast.LENGTH_SHORT).show(); break; case R.id.delete: Toast.makeText(this,"You clicked Delete",Toast.LENGTH_SHORT).show(); break; case R.id.settings: Toast.makeText(this,"You clicked Settings",Toast.LENGTH_SHORT).show(); break; default: } return true; } }
运行后,列表实现了,但同时我们也会发现之前的Toolbar被RecyclerView给挡住了,这个问题又该怎么解决呢?这就需要借助到另外一个工具了——AppBarLayout.
2.AppBarLayout
CoordinatorLayout就是要给加强版的FrameLayout,解决上述遮挡问题传统情况下,使用偏移是唯一的解决办法,即让RecyclerView向下偏移一个Toolbar的高度,从而保证不会遮挡到Toolbar。
这里我准备使用Design Support库中提供的另外一个工具——AppBarLayout。AppBarLayout实际上是一个垂直方向的LinearLayout,它再内部做了很多滚动事件的封装,并应用了一些Material Design的设计理念。
解决步骤:
第一步将Toolbar嵌套到AppBarLayout中,第二步给RecyclerView指定一个布局行为。
主布局代码修改后如下所示:
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/drawaer_layout" > android:layout_width="match_parent" android:layout_height="match_parent" > android:layout_width="match_parent" android:layout_height="wrap_content" > 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" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:layout_scrollFlags="scroll|enterAlways|snap" /> android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@android:drawable/ic_menu_share" app:elevation="8dp" /> android:id="@+id/nav_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" app:menu="@menu/nav_menu" app:headerLayout="@layout/nav_header" />
可以看到,布局文件并没有太大的变化,我们首先定义了一个AppBarLayout,并将Toolbar放置在了AppBarLayout里面,然后再RecyclerView中使用app:layout_behavior属性指定了一个布局行为。其中appbar_scrolling_view_behavior这个字符串也是由Design Support库提供的。这样就解决了RecyclerView遮挡住Toolbar的问题。
看看滚动过程中如何体现Material Design风格的,事实上,当RecyclerView滚动的时候就已经将滚动事件都通知给AppBarLayout了。当AppBarLayout接收到滚动事件的时候,它内部的子控件其实是可以指定如何去影响这些事件的,通过app:layout_scrollFlags属性就能实现。
这里在Toolbar中添加了一个app:layout_scrollFlags属性,并将这个属性的值指定成了scroll|enterAlways|snap。其中,scroll表示当RecyclerView向上滚动的时候,Toolbar会跟着一起向上滚动并实现隐藏;enterAlways表示当RecyclerView向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示;snap表示当Toolbar还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏啊hi是显示。
五、下拉刷新
SwipeRefreshLayout就是用于实现下拉刷新功能的核心类,它是由support-v4库提供的。我们把想要实现下拉刷新功能的控件放置到SwipeRefreshLayout中,就可以迅速让这个控件支持下拉刷新。
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/drawaer_layout" > android:layout_width="match_parent" android:layout_height="match_parent" > android:layout_width="match_parent" android:layout_height="wrap_content" > 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" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:layout_scrollFlags="scroll|enterAlways|snap" /> android:id="@+id/swipe_refresh" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" > android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" /> android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@android:drawable/ic_menu_share" app:elevation="8dp" /> android:id="@+id/nav_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" app:menu="@menu/nav_menu" app:headerLayout="@layout/nav_header" />
这里我们在RecyclerView外面又嵌套了一层SwipeRefreshLayout,这样RecyclerView现在变成了SwipeRefreshLayout的子控件,因此前面使用的app:layout_behavior声明的布局行为现在也要移到SwipeRefreshLayout中才行。
public class MaterialDesignTestActivity extends AppCompatActivity { private DrawerLayout mDrawerLayout; private Fruit[] fruits = {new Fruit("Apple",R.mipmap.ic_launcher),new Fruit("Banana",R.mipmap.ic_launcher), new Fruit("Orange",R.mipmap.ic_launcher),new Fruit("Watermelon",R.mipmap.ic_launcher), new Fruit("Pear",R.mipmap.ic_launcher),new Fruit("Grape",R.mipmap.ic_launcher), new Fruit("Pineapple",R.mipmap.ic_launcher),new Fruit("Strawberry",R.mipmap.ic_launcher), new Fruit("Cherry",R.mipmap.ic_launcher),new Fruit("Mango",R.mipmap.ic_launcher) }; private ListfruitList = new ArrayList<>(); private FruitAdapter adapter; private SwipeRefreshLayout swipeRefresh; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_material_design_test); initUI(); initFruits(); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); GridLayoutManager layoutManager = new GridLayoutManager(this,2); recyclerView.setLayoutManager(layoutManager); adapter = new FruitAdapter(fruitList); recyclerView.setAdapter(adapter); swipeRefresh = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh); swipeRefresh.setColorSchemeResources(R.color.colorPrimary); swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener(){ @Override public void onRefresh() { refreshFruits(); } }); } private void refreshFruits(){ new Thread(new Runnable() { @Override public void run() { try{ Thread.sleep(2000); }catch(InterruptedException e){ e.printStackTrace(); } runOnUiThread(new Runnable() { @Override public void run() { initFruits(); adapter.notifyDataSetChanged();; swipeRefresh.setRefreshing(false); } }); } }).start(); } public void initUI(){ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawaer_layout); NavigationView navView = (NavigationView) findViewById(R.id.nav_view); ActionBar actionBar = getSupportActionBar(); if(actionBar!=null){ actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.mipmap.ic_launcher); } navView.setCheckedItem(R.id.nav_call); navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener(){ @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { mDrawerLayout.closeDrawers(); return true; } }); /*findViewById(R.id.tv_menu).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mDrawerLayout.closeDrawers(); } });*/ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view) { Snackbar.make(view,"Data delete", Snackbar.LENGTH_SHORT). setAction("Undo",new View.OnClickListener(){ @Override public void onClick(View view) { Toast.makeText(MaterialDesignTestActivity.this,"Data restored",Toast.LENGTH_SHORT).show(); } }).show(); } }); } private void initFruits(){ fruitList.clear(); for(int i=0;i<50;i++){ Random random = new Random(); int index = random.nextInt(fruits.length); fruitList.add(fruits[index]); } } public boolean onCreateOptionsMenu(Menu menu){ getMenuInflater().inflate(R.menu.toolbar,menu); return true; } public boolean onOptionsItemSelected(MenuItem item){ switch(item.getItemId()){ case android.R.id.home: mDrawerLayout.openDrawer(GravityCompat.START); break; case R.id.backup: Toast.makeText(this,"You clicked Backup",Toast.LENGTH_SHORT).show(); break; case R.id.delete: Toast.makeText(this,"You clicked Delete",Toast.LENGTH_SHORT).show(); break; case R.id.settings: Toast.makeText(this,"You clicked Settings",Toast.LENGTH_SHORT).show(); break; default: } return true; } }
六、可折叠式标题栏
1.CollapsingToolbarLayout
CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它也是由Design Support库提供的。CollapsingToolbarLayout可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,而是能够实现非常华丽的效果。
不过,CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是CoordinatorLayout的子布局,因此实现该功能要综合运用前面所学的各种知识。
eg.实现列表详情展示页
fruit.xml布局页代码如下:
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.eastelsoft.nuonuo.activity.FruitActivity"> android:id="@+id/appBar" android:layout_width="match_parent" android:layout_height="250dp" > android:id="@+id/collapsing_toolbar" android:layout_width="match_parent" android:layout_height="match_parent" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" > android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/fruit_image_view" android:scaleType="centerCrop" app:layout_collapseMode="parallax" /> android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" > android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" > android:layout_width="match_parent" android:layout_height="wrap_content"> android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="15dp" android:layout_marginLeft="15dp" android:layout_marginRight="15dp" android:layout_marginTop="35dp" app:cardCornerRadius="4dp" > android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/fruit_content_text" android:layout_margin="10dp" /> android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@android:drawable/ic_menu_share" app:layout_anchor="@id/appBar" app:layout_anchorGravity="bottom|end" />
app:contentScrim属性用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色,其实CollapsingToolbarLayout在折叠之后就是一个普通的Toolbar,那么背景色肯定应该是colorPrimary了。
app:layout_scrollFlags属性之前用过,scroll表示CollapsingToolbarLayout会随着水果内容详情的滚动一起滚动,exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
app:layout_collapseMode,用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式,其中Toolbar指定成pin,表示在折叠过程中位置始终保持不变,ImageView指定成parallax,表示会在折叠的过程中产生一定的错位偏移,这种模式的视觉效果会非常好。
NestedScrollView,和AppBarLayout是平级的,ScrollView允许使用滚动的方式来查看屏幕意外的数据,而NestedScrollView在此基础上还增加了嵌套响应滚动事件的功能。由于CoordinatoLayout本身已经可以响应滚动事件了,因此我们在它的内部就需要使用NestedScrollView或RecyclerView这样的布局。另外,这里还通过app:layout_behavior属性指定了一个布局行为。
可以看到,这里加入了一个FloatingActionButton,它和AppBarLayout以及NestedScrollView是平级的,FloatingActionButton中使用app:layout_anchor属性指定了一个锚点,这里将锚点设置为AppBarLayout,这样悬浮按钮就会出现在水果标题栏的区域内,接着又使用app:layout_anchorGravity属性将悬浮按钮定位在标题栏区域的右下角。
FruitActivity代码如下:
public class FruitActivity extends AppCompatActivity { public static final String FRUIT_NAME="fruit_name"; public static final String FRUIT_IMAGE_ID="fruit_image_id"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fruit); Intent intent = getIntent(); String fruitName = intent.getStringExtra(FRUIT_NAME); int fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID,0); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); CollapsingToolbarLayout collapsingToolbar = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); ImageView fruitImageView = (ImageView) findViewById(R.id.fruit_image_view); TextView fruitContentText = (TextView) findViewById(R.id.fruit_content_text); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); if(actionBar!=null){ actionBar.setDisplayHomeAsUpEnabled(true); } collapsingToolbar.setTitle(fruitName); Glide.with(this).load(fruitImageId).into(fruitImageView); String fruitContent = generateFruitContent(fruitName); fruitContentText.setText(fruitContent); } private String generateFruitContent(String fruitName){ StringBuilder fruitContent = new StringBuilder(); for(int i=0;i<500;i++){ fruitContent.append(fruitName); } return fruitContent.toString(); } public boolean onOptionsItemSelected(MenuItem item){ switch(item.getItemId()){ case android.R.id.home: finish(); return true; } return super.onOptionsItemSelected(item); } }
FruitAdapter中添加点击事件:
public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType){ if(mContext==null){ mContext=parent.getContext(); } View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item,parent,false); final ViewHolder holder = new ViewHolder(view); holder.cardView.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view) { int position = holder.getAdapterPosition(); Fruit fruit = mFruitList.get(position); Intent intent = new Intent(mContext, FruitActivity.class); intent.putExtra(FruitActivity.FRUIT_NAME,fruit.getName()); intent.putExtra(FruitActivity.FRUIT_IMAGE_ID,fruit.getImageId()); mContext.startActivity(intent); } }); return holder; }
2.充分利用系统状态栏空间
虽然说现在水果详情展示界面的效果已经非常华丽了,但着并不代表我们不能再进一步地提升。观察一下,你会发现水果地背景图片和系统地状态栏总有一些不搭地感觉,如果我们能将背景图和状态栏融合到一起,那这个视觉体验绝对能提升好几个档次。
只不过很可惜地是,在Android5.0系统之前,我们是无法对状态栏地背景或颜色进行操作地,那个时候也还没有Material Design的概念。但是Android 5.0及之后的系统都是支持这个功能的,因此这里我们就来实现一个系统差异型的效果,在 Android5.0及之后的系统中,使用背景图和状态栏融合的模式,在之前的系统中使用普通的模式。
想要让背景图能够和系统状态栏融合,需要借助android:fitsSystemWindows这个属性来实现。在CoordinatorLayout、AppBarLayot、CollapsingToolbarLayout这种嵌套结构的布局中,将控件的android:fitsSystemWindows属性指定成true,就表示该控件会出现在系统状态栏里。对应到我们的程序,那就是水果标题栏中的ImageView应该设置成这个属性,不过只给ImageView设置成这个属性是没用的,我们必须将ImageView布局结构中的所有父布局都设置上这个属性才可以。
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.eastelsoft.nuonuo.activity.FruitActivity" android:fitsSystemWindows="true" > android:id="@+id/appBar" android:layout_width="match_parent" android:layout_height="250dp" android:fitsSystemWindows="true" > android:id="@+id/collapsing_toolbar" android:layout_width="match_parent" android:layout_height="match_parent" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" android:fitsSystemWindows="true" > android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/fruit_image_view" android:scaleType="centerCrop" app:layout_collapseMode="parallax" android:fitsSystemWindows="true" /> android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" > android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" > android:layout_width="match_parent" android:layout_height="wrap_content"> android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="15dp" android:layout_marginLeft="15dp" android:layout_marginRight="15dp" android:layout_marginTop="35dp" app:cardCornerRadius="4dp" > android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/fruit_content_text" android:layout_margin="10dp" /> android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@android:drawable/ic_menu_share" app:layout_anchor="@id/appBar" app:layout_anchorGravity="bottom|end" />
xml version="1.0" encoding="utf-8"?>
<activity android:name=".activity.FruitActivity" android:theme="@style/FruitActivityTheme" >activity
这里我们充分利用了Design Support库、support-v4库、appcompat-v7库,以及一些开源项目来实现一个高度Material化的应用程序。不过说到底,我让然还是在以一个开发者的思维给你讲解Material Design,侧重于如何去实现这些效果。而实际上,Material Design的设计思维和设计理念才是更加重要的东西,当然这部分内容应该是UI设计人员去学习的,如果你也感兴趣的化,可以参考一下Material Design的官方文章:https://material.google.com。