Google I/O 2014 发布了Material Design。希望统一 Android平台设计语言规范。然而对于国内的很多产品和设计师而言,并没有对此产生多大的兴趣,也许是习惯,也许是觉得android的碎片化太严重不愿意花时间精力去做适配,所以更多的还是采用IOS的风格,导致尽管Material Design推出了很长时间但是在国内使用的还是比较少。但是就风格层面而言,我觉得Material Design所设计的还是非常不错的,特别是很多新推出的控件,使用起来感觉还是挺不错的,因此,还是很有必要学习一下关于Material Design的知识。
Android Material Design新增常用控件
1、ToolBar和Menu配合使用代替ActionBar
2、基于CoordinatorLayout的联动
3、侧滑抽屉NavigationView
4、卡片布局CardView
5、RecyclerView(在本篇里面不介绍,下一篇单独介绍)
6、TabLayout(配合Fragment使用)
7、弹出提醒SnackBar
8、FloatingActionButton
Toobar
准备工作
使用Toolbar之前需要引入依赖
implementation 'androidx.appcompat:appcompat:1.3.0'
Toolbar是android 5.0推出的一个新的导航控件用来取代传统的ActionBar控件。需要注意的是,如果使用Toolbar,需要先将系统的ActionBar去掉,可以通过主题将我们的父级主题设置为NoActionBar,如下所示
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:navigationBarColor">@android:color/holo_orange_dark</item>
<item name="android:colorControlHighlight">@android:color/holo_blue_bright</item>
</style>
使用步骤
1、布局里面进行声明
<androidx.appcompat.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"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
其中 android:theme主题是Toolbar自身的主题,用于显示的样式,那么app:popupTheme主题是干嘛的呢?其实这和后面讲的Menu有关,我们知道ToolBar很多时候是和Menu一起配合使用的,那么app:popupTheme其实就是定义弹出的Menu的样式。
2、代码里面设置Toolbar
setSupportActionBar(toolbar)
由于我们的父级theme已经将actionBar隐藏掉了,所以我们直接设置就行了。
3、通过代码动态设置Toolbar的属性(也可以直接在XML文件里面进行设置)
private void setToolbarProperty() {
// 设置正标题
toolbar.setTitle("正标题");
// 设置副标题
toolbar.setSubtitle("副标题");
// 设置左边按钮图片
toolbar.setNavigationIcon(R.mipmap.ic_launcher_round);
// 设置(Log)标题与左边按钮之间图标
toolbar.setLogo(R.mipmap.ic_launcher);
}
4、添加Menu
首先必须在Activity重写onCreateOptionsMenu方法,添加Menu的操作都在这个方法中执行。
/**
* 创建菜单
*/
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.designer, menu)
return true
}
其中designer的布局文件为
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/icon_one"
android:icon="@drawable/item"
android:title="目录"
app:showAsAction="always" />
<item
android:id="@+id/icon_two"
android:icon="@drawable/share"
android:title="分享"
app:showAsAction="ifRoom" />
<item
android:id="@+id/icon_three"
android:icon="@drawable/setting"
android:title="设置"
app:showAsAction="never" />
</menu>
在这里面需要重点强调的一点就是showAction属性,它所不同的取值其所代表的意思是:
app:showAsAction="always/ifRoom/never"
always表示永远显示在Toolbar中
ifRoom表示屏幕空间足够的情况下显示在Toolbar中,不够的话就显示在菜单中
never表示永远显示在菜单中
5、监听Menu的点击事件
复写onOptionsItemSelected方法,在这里通过View的ID去执行其所对应的点击事件
/**
* 响应菜单的点击事件
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.icon_one -> drawer_layout.openDrawer(GravityCompat.START)
R.id.icon_two -> Toast.makeText(this, "你点击了第二个菜单", Toast.LENGTH_SHORT).show()
R.id.icon_three -> Toast.makeText(this, "你点击了第三个菜单", Toast.LENGTH_SHORT).show()
}
return true
}
基于CoordinatorLayout的联动
CoordinatorLayout的联动算的上是Material Designer的一大亮点,对于CoordinatorLayout来说,其重要的作用就是实现协调其他组件实现联动的效果。
其布局文件为
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:fitsSystemWindows="true"
tools:context=".FruitActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/colorPrimaryDark"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="@+id/fruit_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="35dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
app:cardCornerRadius="4dp">
<TextView
android:id="@+id/fruit_content_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
既然是协调其他组件,那么CoordinatorLayout肯定是作为最顶层布局进行放置。至于其他的组件,这里大概介绍一下:
(1)AppBarLayout:一种支持响应滚动手势的app bar布局,AppBarLayout 继承自LinearLayout,布局方向为垂直方向。所以你可以把它当成垂直布局的LinearLayout来使用。AppBarLayout是在LinearLayou上加了一些材料设计的概念,它可以让你定制当某个可滚动View的滚动手势发生变化时,其内部的子View实现何种动作。
(2)CollapsingToolbarLayout:可折叠的Toolbar。
(3)NestedScrollView:支持嵌套滑动的ScrollView。NestedScrollView与 ScrollView的区别就在于 NestedScrollView支持嵌套滑动,无论是作为父控件还是子控件,嵌套滑动都支持,且默认开启。因此,在一些需要支持嵌套滑动的情景中,比如一个 ScrollView内部包裹一个 RecyclerView,那么就会产生滑动冲突,这个问题就需要你自己去解决。而如果使用 NestedScrollView包裹 RecyclerView,嵌套滑动天然支持,你无需做什么就可以实现前面想要实现的功能了。
(4)CardView:卡片布局(下面会讲到)。
那么,CoordinatorLayout到底是如何进行联动的呢,它对其他组件进行联动的依据又是什么呢?答案其实很简单,就是通过layout_scrollFlags。layout_scrollFlags的取值有很多,具体来说:
(1)scroll:Child View 伴随着scrollingView的滚动事件而滚出或滚进屏幕。需要强调的是如果使用了其他值,必定要使用这个值才能起作用(比如你想使用enterAlways,则必须使用scroll,否则enterAlways无效)。
(2)exitUntilCollapsed:当你定义了一个minHeight,这个view将在滚动到达这个最小高度的时候消失。
(3)enterAlways:一旦向上滚动这个view就可见。
(4)enterAlwaysCollapsed:当你定义了一个minHeight,那么view将在到达这个最小高度的时候开始显示,并且从这个时候开始慢慢展开,当滚动到顶部的时候展开完。
当然,对于NestedScrollView控件来说,它必须添加app:layout_behavior="@string/appbar_scrolling_view_behavior”,这个属性用于通知AppBarLayout,NestedScrollView何时发生了滚动事件。
基于上面的例子可以总结出CoordinatorLayout联动的两点规律:
1、父布局肯定是CoordinatorLayout
2、一定会设置app:layout_scrollFlags和app:layout_behavio两个属性。滑动的view官方建议使用NestedScrollView或者RecyclerView。ListView在5.0以下就没有效果了。
侧滑抽屉NavigationView
1.将DrawerLayout作为根布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MaterialDesignerActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.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"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="30dp"
android:src="@drawable/action"
app:elevation="8dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header"
app:menu="@menu/nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>
观察布局文件可知,在DrawerLayout布局下有两个视图,一个就是我们刚刚讲到的CoordinatorLayout,还有一个是NavigationView,那么这个NavigationView是做什么用的呢?
NavigationView是Google在5.0之后推出布局控件,放在DrawerLayout中来使用从而实现侧拉效果。DrawerLayout关键的两行布局代码
app:headerLayout="@layout/nav_header"
app:menu="@menu/nav_menu"
那么这两行是什么意思呢?看下面图就明白了
其中headerLayout的布局为
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="180dp"
android:background="?attr/colorPrimary"
android:padding="10dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/icon_image"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_centerInParent="true"
android:src="@drawable/default_icon" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="zhoufan@gmail.com"
android:textSize="14sp"
android:textColor="#FFF"/>
<TextView
android:id="@+id/mail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/username"
android:text="Green"
android:textSize="14sp"
android:textColor="#FFF"/>
</RelativeLayout>
与我们平时写的布局一致。
Menu布局是一个菜单布局
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_call"
android:icon="@drawable/call"
android:title="Call" />
<item
android:id="@+id/nav_friend"
android:icon="@drawable/friend"
android:title="Friend" />
<item
android:id="@+id/nav_location"
android:icon="@drawable/location"
android:title="Location" />
<item
android:id="@+id/nav_mail"
android:icon="@drawable/mail"
android:title="Mail" />
</group>
</menu>
布局设置完成后效果就出来了,手指向右滑动的时候就会把NavigationView所包裹的内容展示出来,当然,你也可以通过代码点击将NavigationView所包裹的内容展示出来。具体的实现为:
// 打开抽屉
drawer_layout.openDrawer(GravityCompat.START)
// 关闭抽屉
nav_view.setNavigationItemSelectedListener {
drawer_layout.closeDrawers()
true
}
卡片布局CardView
CardView是继承于FrameLayout,用于布局的圆角,阴影等效果的实现,其基本的属性为:
方法 | 含义 |
---|---|
属性 | 作用 |
cardElevation | 阴影的大小 |
cardCornerRadius | 卡片的圆角大小 |
contentPadding | 卡片内容于边距的间隔 |
如果直接给CardView添加android:foreground="?attr/selectableItemBackground"就会加上水波纹点击反馈。
TabLayout(配合Fragment使用)
在众多的APP当中,这种效果已经很普遍了,具体来说,就是使用TabLayout + Fragment来实现,其实现的过程为:
1.创建布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:background="@color/windowBackground"
android:orientation="vertical">
<com.stkj.baselibrary.commonitem.UserItem
android:id="@+id/search_detail_all_item"
android:layout_width="match_parent"
android:layout_height="@dimen/dp44"
android:layout_marginTop="@dimen/dp44"
app:item_left_icon_res="@mipmap/back"
app:item_left_icon_visible="true"
app:item_middle_text_color="@color/color333333"
app:item_middle_text_size="@dimen/dp17"
app:item_middle_text_visible="true" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/search_detail_all_tab"
android:layout_width="match_parent"
android:layout_height="@dimen/dp44"
android:background="@color/windowBackground"
app:tabIndicatorColor="@color/colorE6B536"
app:tabIndicatorHeight="@dimen/dp2"
app:tabTextAppearance="@style/App_Theme"
app:tabTextColor="@color/color999999"
app:tabBackground="@android:color/transparent"
app:tabRippleColor="@android:color/transparent"
app:tabIndicatorFullWidth="false"
app:tabSelectedTextColor="@color/colorE6B536"/>
<androidx.viewpager.widget.ViewPager
android:id="@+id/search_detail_all_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
2.设置适配器
public class TypeSearchAdapter extends FragmentStatePagerAdapter {
private FragmentManager mFragmentManager;
//保存每个Fragment的Tag,刷新页面的依据
private List<Fragment> fragments;
private String[] mTitles;
public TypeSearchAdapter(FragmentManager fm,List<Fragment> fragments, String[] titles) {
super(fm);
this.fragments = fragments;
this.mTitles = titles;
mFragmentManager = fm;
}
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments != null ? fragments.size() : 0;
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return mTitles[position];
}
}
注意这里使用的是FragmentStatePagerAdapter而非FragmentPagerAdapter。至于为什么,大家可以自行去看看二者的区别在哪。
3.关联
private void init() {
String[] mTabList = new String[list.size()];
for (int i = 0; i < list.size(); i++) {
String title = list.get(i).getTitle();
mTabList[i] = title;
mFragmentList.add(TypeSearchDetailAllFragment.newInstance(String.valueOf(list.get(i).getCategoryID()),mTitle,mTitleType));
if (title.equals(mTitle)) {
mPosition = i;
}
}
mAdapter = new TypeSearchAdapter(getSupportFragmentManager(), mFragmentList, mTabList);
mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
mViewPager.setAdapter(mAdapter);
mTabLayout.setupWithViewPager(mViewPager);
mViewPager.setCurrentItem(mPosition, false);
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
userItem.getMiddleTv().setText(mTabList[position]);
mTitle = mTabList[position];
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
首先实例化好我们需要加载的Fragment,然后通过适配器将它与TabLayout进行关联。这里有两行比较重要的代码
mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
mTabLayout.setupWithViewPager(mViewPager);
第一行代码的意思是设置TabLayout的滚动模式,其可选值有两种:
1》TabLayout.MODE_SCROLLABLE:当元素过多时会超出父布局,并可以滑动Tab,Tab的宽度为Tab的实际宽度。
2》TabLayout.MODE_FIXED:无论界面由多少元素都会充满父布局。并且平均分配Tab的宽度。
第二行代码的意思是将TabLayout与ViewPager相关联,因为ViewPager填充的是Fragment,所以相当于TabLayout与Fragment实现了联动,也就是我们上面看到的效果。当然,除此之外,你还可以在布局文件里面设置TabLayout的一些属性,具体的属性为:
方法 | 含义 |
---|---|
tabSelectedTextColor | 设置TabLayout选中时候的颜色 |
tabTextColor | 设置TabLayout未选中时候的颜色 |
tabIndicatorColor | 设置TabLayout指示器的颜色 |
tabBackground | 设置TabLayout背景的颜色 |
tabTextAppearance | 设置TabLayout内部字体大小和样式 |
tabIndicatorHeight | 设置TabLayout指示器的高度 |
tabRippleColor | 设置TabLayout条目点击时候的颜色 |
tabIndicatorFullWidth | 设置TabLayout是否填充满整个宽度 |
弹出提醒Snackbar
Snackbar的使用和Toast基本类似,其基本的使用为:
仅仅只是提示文字弹出提示文字之后一段时间后会消失。
Snackbar.make(button, "第一次使用SnackBar", Snackbar.LENGTH_SHORT).show();
提供一个可以点击的按钮
Snackbar.make(it, "Data deleted", Snackbar.LENGTH_SHORT).setAction(
"Undo"
) {
Toast.makeText(this@MaterialDesignerActivity, "Data restored", Toast.LENGTH_SHORT)
.show()
}.show()
必须和用户交互之后才消失
Snackbar.make()设置时间的参数改为:Snackbar.LENGTH_INDEFINITE。如果不设置这个参数,Snackbar都会在一段时间后消失。
Snackbar.make(button, "第一次使用SnackBar", Snackbar.LENGTH_INDEFINITE).setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
}
}).show();
添加状态回调,监听Snackbar的展示和消失
Snackbar.make(button, "第一次使用SnackBar", Snackbar.LENGTH_INDEFINITE).setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
}
}).addCallback(new Snackbar.Callback() {
@Override
public void onShown(Snackbar sb) {
super.onShown(sb);
Toast.makeText(MainActivity.this, "弹出了", Toast.LENGTH_SHORT).show();
}
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
super.onDismissed(transientBottomBar, event);
Toast.makeText(MainActivity.this, "消失了", Toast.LENGTH_SHORT).show();
}
}).show();
当然,在实际的开发过程中,Snackbar的使用还是比较少的,一般还是更加习惯使用Toast,如果涉及到交互的,很多时候会考虑使用AlertDialog或者PopupWindow而不是Snackbar。
FloatingActionButton
界面浮动的标签,一般用于页面关键功能入口。FloatingActionButton非常简单,知道了解FloatingActionButton的一些属性和点击回调即可。
方法 | 含义 |
---|---|
fabSize | 定义FloatingActionButton的大小。auto(大) mini(小) normal(中) |
elevation | 普通状态下的阴影深度 |
pressedTranslationZ | 按下时的阴影深度 |
backgroundTint | 默认展示的背景颜色 |
rippleColor | 按下时的颜色(5.0以后为水波纹的颜色) |
layout_anchor | 定位其他控件,和其他控件边界相交 |
layout_anchorGravity | 和layout_anchor属性联用,在其他控件的相对位置 |
useCompatPadding | 设置内边距 |
点击事件监听
FloatingActionButton fabOne = (FloatingActionButton)findViewById(R.id.fabOne);
fabOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Snackbar.make(v, "FloatingActionButton 被点击", Snackbar.LENGTH_SHORT).show();
}
});