1、Toolbar
1.1 修改样式#####
首先要知道,任何一个新建项目默认都是ActionBar,这个ActionBar就是根据项目中指定的主题来显示,打开AndroidManifest.xml文件看一下,如下所示:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme" >
...
</application>
可以看到,这里使用 android:theme属性指定一个AppTheme的主题,该属性定义在res/values/styles.xml文件中,如下所示:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
这里定义了一个叫AppTheme的主题,然后指定他的parent主题是Theme.AppCompat.Light.DarkActionBar。这个DarkActionBar是一个深色的ActionBar主题。而现在我们使用Toolbar代替ActionBar,因此需要指定一个不带ActionBar的主题,通常有两种主题可选
- Theme.AppCompat.NoActionBar 表示深色主题
- Theme.AppCompat.Light.NoActionBar 表示淡色主题
这里我们选用淡色主题,如下所示:
<resources>
<!-- Base application theme. -->
<!-- 淡色主题 parent="Theme.AppCompat.Light.NoActionBar"-->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
这里重写了colorPrimary、colorPrimaryDark、colorAccent 3个属性。
用下面的图来解释每个属性代表的含义:
唯独colorAccent属性需要强调一下:它不只是指定这个按钮的颜色,而是表达强调的的意思,比如一些控件的选中状态也会使用colorAccent的颜色。
1.2 修改成Toolbar#####
当在res/values/styles.xml文件中将 parent属性设置为"Theme.AppCompat.Light.NoActionBar"后,ActionBar就隐藏起来了。
现在用Toolbar代替ActionBar:
修改activity_main.xml
<FrameLayout 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="match_parent">
<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"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>
- Toolbar控件是appcompat-v7库提供的,高度设置为actionBar的高度。
- Toolbar上面的元素默认使用深色系,为了让Toolbar和界面风格一致,将样式调整为android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"。
- 如果Toolbar有菜单按钮,指定淡色主题
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" 。
app:popupTheme属性是Android5.0 系统新增的,使用app:popupTheme可以兼容以下版本。
同时修改MainActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
}
}
同时给activity增加了一个android:label属性,用于指定在Toolbar中显示文字的内容,如果没有指定默认使用application中指定label内容。
<activity
android:name=".MainActivity"
android:label="材料设计">
...
</activity>
效果如下:
1.3 在Toolbar上加入Action按钮#####
在res目录下新建toolbar.xml的menu文件夹,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/backup"
android:icon="@drawable/ic_backup"
android:title="Backup"
app:showAsAction="always" />
<item
android:id="@+id/delete"
android:icon="@drawable/ic_delete"
android:title="Delete"
app:showAsAction="ifRoom" />
<item
android:id="@+id/settings"
android:icon="@drawable/ic_settings"
android:title="Settings"
app:showAsAction="never" />
</menu>
app:showAsAction属性用来指定按钮显示位置,showAsAction有如下几种值可选:
- always :永远显示在Toolbar中
- ifRoom :屏幕空间足够的情况下显示在Toolbar中,不够的话显示在菜单中
- never :永远显示在菜单中
** 注意:Toolbar中的action按钮只会显示图片,菜单中的action按钮只会显示文字 **
修改MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar, menu);
return true;
}
@Override
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;
}
return true;
}
}
在onCreateOptionsMenu方法中加载toolbar.xml菜单文件,然后在onOptionsItemSelected方法处理按钮的点击事件
2、滑动菜单DrawerLayout
DrawerLayout是个布局,在布局中允许放两个直接子控件,第一个子控件是主屏幕中显示的内容,第二个子控件是滑动菜单中显示的内容。
修改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_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<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"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#FFF"
android:text="This is menu"
android:textSize="30sp" />
</android.support.v4.widget.DrawerLayout>
这里最外层的控件使用了DrawerLayout,这个控件是support-4库提供。DrawerLayout中放置了两个直接子控件,第一个子控件是FrameLayout,用于作为主屏幕中显示的内容;第二个子控件使用了TextView,用于作为滑动菜单中显示的内容。
第二个子控件有一点特别要注意,android:layout_gravity="start"
必须要指定。因为要告诉DrawerLayout滑动菜单是在屏幕的左边还是右边。指定left表示滑动菜单在左边,right表示滑动菜单在右边。这里使用过了start,表示会根据系统语言进行判断。如果系统语言是从左往右的,比如汉语、英语,滑动菜单就在左边。
Material Design建议在Toolbar的最左边加入一个导航按钮,点击按钮后也会将滑动菜单内容展示出来。
准备一张ic_menu.png的图片后,修改MainActivity后如下:
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
mDrawerLayout.openDrawer(GravityCompat.START);
break;
...
}
return true;
}
}
首先获得DrawerLayout的实例,然后调用getSupportActionBar()方法得到ActionBar的实例,虽然这个ActionBar的具体实现是由Toolbar完成的。
接着调用ActionBar的setDisplayHomeAsUpEnabled()方法让导航按钮显示出来,又调用了setHomeAsUpIndicator()方法设置一个导航按钮图标。实际上,Toolbar最左侧这个按钮叫做HomeAsUp按钮,它默认的图标是一个返回的箭头,含义是返回上一个活动。
在onOptionsItemSelected()方法中对HomeAsUp按钮的点击事件进行处理,** HomeAsUp按钮的id永远都是android.R.id.home **。
然后调用DrawerLayout的openDrawer()方法将滑动菜单显示出来。注意:openDrawer()方法要求传入一个Gravity参数,为了保证这里的行为和XML定义的一致,这里传入了GravityCompat.START。
效果如下:
3、NavigationView
通过NavigationView可以在滑动菜单页面定制任意布局。NavigationView是Design Support库中提供的。需要在app/builde.gradle文件中加入依赖
dependencies {
...
compile 'com.android.support:design:25.0.1'
compile 'de.hdodenhof:circleimageview:2.1.0'
...
}
在使用NavigationView之前,需要提供menu和headerLayout。
- menu是用来在NavigationView中显示具体的菜单项。
- headerLayout是用来在NavigationView中显示头部布局。
在menu文件夹中加入nav_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_call"
android:icon="@drawable/nav_call"
android:title="Call" />
<item
android:id="@+id/nav_friends"
android:icon="@drawable/nav_friends"
android:title="Friends" />
<item
android:id="@+id/nav_location"
android:icon="@drawable/nav_location"
android:title="Location" />
<item
android:id="@+id/nav_mail"
android:icon="@drawable/nav_mail"
android:title="Mail" />
<item
android:id="@+id/nav_task"
android:icon="@drawable/nav_task"
android:title="Tasks" />
</group>
</menu>
在menu中嵌套了一个<group>标签,group表示一个组,android:checkableBehavior指定为single表示组中的所有菜单项都是单选。
在layout文件夹中加入nav_header.xml
<?xml version="1.0" encoding="utf-8"?>
<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">
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="tonygreendev@gmail.com"
android:textColor="#FFF"
android:textSize="14sp" />
<TextView
android:id="@+id/mail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/username"
android:text="Tony Green"
android:textColor="#FFF"
android:textSize="14sp" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/icon_image"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:src="@drawable/nav_icon" />
</RelativeLayout>
修改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_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<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"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>
<android.support.design.widget.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" />
</android.support.v4.widget.DrawerLayout>
这里把之前的TextView换成了NavigationView,然后通过app:headerLayout和app:menu属性把刚才准备好的menu和headerLayout设置进去了。
同时也要处理菜单项的点击事件才行
修改MainActivity如下:
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
NavigationView navView = (NavigationView) findViewById(R.id.nav_view);
navView.setCheckedItem(R.id.nav_call);
navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
mDrawerLayout.closeDrawers();
return true;
}
});
}
效果如下:
4、FloatingActionButton
FloatingActionButton是Design Support库提供,可以实现悬浮按钮。默认使用colorAccent作为按钮的颜色,可以指定一个图标来表示这个按钮的作用。
修改activity_main.xml如下:
<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_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<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"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<android.support.design.widget.FloatingActionButton
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="@drawable/ic_done" />
</FrameLayout>
...
</android.support.v4.widget.DrawerLayout>
修改MainActivity如下:
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this,"点击悬浮按钮了",Toast.LENGTH_LONG).show();
}
});
}
...
}
显示效果如下:
5、Snackbar
Snackbar是一个提示工具,在Design Support库中。
Snackbar并不是Toast的替代品,两者之间有着不同的应用场景。Toast的作用是告诉用户现在发生了什么事,但同时用户只能被动接收这个事情。而Snackbar则进行了扩展,在提示中加入了可交互按钮,当用户点击按钮时可以执行额外的逻辑操作。
修改MainActivity如下:
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "是否删除", Snackbar.LENGTH_INDEFINITE)
.setAction("撤销", new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "数据已恢复", Toast.LENGTH_SHORT).show();
}
})
.show();
}
});
}
}
...
这里调用了Snackbar的make()方法创建一个Snackbar对象,该方法第一个参数需要传入一个view,只要是当前界面布局的任意一个View都可以,Snackbar会使用这个View来自动查找最外层的布局,用于展示Snackbar。第二个和第三个参数不用解释。
接着调用setAction()方法来设置一个动作,从而让Snackbar不仅仅是一个提示,而是可以和用户交互的。
效果如下:
可以看到Snackbar从屏幕底部出现了,但是这个Snackbar竟然把悬浮按钮遮挡住了,尼玛啊!只需借助CoordinatorLayout就可以轻松搞定。
6、CoordinatorLayout
CoordinatorLayout是一个加强版的FrameLayout,在Design Support库中。CoordinatorLayout可以监听其所有子控件的各种事件,然后自动帮助我们做出最为合理的响应。
举个例子,刚才弹出的Snackbar提示将悬浮按钮遮挡了,如果能让CoordinatorLayout监听到Snackbar的弹出事件,那么它会自动将内部的FloatingActionBar向上偏移,从而确保不会被Snackbar遮挡住。
至于如何使用,只要把原来的FrameLayout替换一下就可以了。
修改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_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<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"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<android.support.design.widget.FloatingActionButton
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="@drawable/ic_done" />
</android.support.design.widget.CoordinatorLayout>>
...
</android.support.v4.widget.DrawerLayout>
效果如下:
刚才说的是CoordinatorLayout可以监听其所有子控件的各种事件,但是Snackbar貌似并不是CoordinatorLayout的子控件啊,为什么它却可以监听到呢?
原因:在Snackbar的make()方法中传入第一个参数view,这个参数就是用来指定Snackbar是基于哪个View来触发的,刚才我们传入的是FloatingActionBar本身,而FloatingActionBar是CoordinatorLayout中的子控件,因此这个事件就理所当然能被监听到了。假如传一个DrawerLayout,那么Snackbar就会再次遮挡悬浮按钮,因为DrawerLayout不是CoordinatorLayout的子控件,CoordinatorLayout也就无法监听到Snackbar的弹出和隐藏事件了。so easy~~~
7、CardView 卡片式布局
CardView 是用于实现卡片式布局的重要控件,由appcompat-v7库提供。实际上CardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果。
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="5dp"
app:cardCornerRadius="4dp">
<TextView
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" />
</android.support.v7.widget.CardView>
app:cardCornerRadius属性用于指定卡片圆角的弧度,数值越大,圆角的弧度也越大。app:elevation属性指定卡片的高度,高度越大,投影范围也越大。
然后在CardView里放置了一个TextView,那么这个TextView就会显示在一张卡片当中了。
下面来实现水果列表,用到了RecyelerView、CardView。在app/build.gradle文件中声明这些依赖库才行:
compile 'com.android.support:recyclerview-v7:25.0.1'
compile 'com.android.support:cardview-v7:25.0.1'
compile 'com.github.bumptech.glide:glide:3.7.0'
修改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_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<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"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.FloatingActionButton
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="@drawable/ic_done" />
</android.support.design.widget.CoordinatorLayout>>
...
</android.support.v4.widget.DrawerLayout>
在CoordinatorLayout中添加一个RecycleView,这样RecycleView就会占满这个布局。
接着定义一个Fruit实体类:
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 int getImageId() {
return imageId;
}
}
在layout目录下加入fruit_item.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="match_parent"
android:layout_height="100dp"
android:scaleType="centerCrop" />
<TextView
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" />
</LinearLayout>
</android.support.v7.widget.CardView>
接下来准备一个适配器
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private Context mContext;
private List<Fruit> mFruitList;
public FruitAdapter(List<Fruit> mFruitList) {
this.mFruitList = mFruitList;
}
static class ViewHolder extends RecyclerView.ViewHolder {
private CardView cardView;
private ImageView fruitImage;
private TextView fruitName;
public ViewHolder(View view) {
super(view);
cardView = (CardView) view;
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
@Override
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);
return holder;
}
@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);
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
修改MainActivity
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
private Fruit[] fruits = {new Fruit("Apple", R.drawable.apple), new Fruit("Banana", R.drawable.banana),
new Fruit("Orange", R.drawable.orange), new Fruit("Watermelon", R.drawable.watermelon),
new Fruit("Pear", R.drawable.pear), new Fruit("Grape", R.drawable.grape),
new Fruit("Pineapple", R.drawable.pineapple), new Fruit("Strawberry", R.drawable.strawberry),
new Fruit("Cherry", R.drawable.cherry), new Fruit("Mango", R.drawable.mango)};
private List<Fruit> fruitList = new ArrayList<>();
private FruitAdapter adapter;
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]);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
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);
}
...
}
效果如下:
screenshot.jpg
这样图片就展示出来了,每个水果都是在一个CardView中,并且还有圆角和阴影。但是,发现Toolbar被遮挡了,擦擦擦~~~这时就需要AppBarLayout来解决了。
8、AppBarLayout
首先来分析一下为什么RecycleView会把Toolbar给遮挡住!
原因:由于RecycleView和Toolbar都放置在CoordinatorLayout中,而CoordinatorLayout是一个加强版的FrameLayout,那么FrameLayout中的所有控件在不进行明确定位情况下,默认都会摆放在布局的左上角,所以产生了遮挡。
传统情况下,使用偏移是唯一解决的办法,既让RecycleView向下偏移一个Toolbar的高度,从而保证不会遮挡到Toolbar。
这里使用AppBarLayout,Design Support库提供。实际上AppBarLayout是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装,并使用了一些Material Degisn的设计理念。
使用AppBarLayout只需两步就可以解决刚才的遮挡问题!
- 第一步将Toolbar嵌套到AppBarLayout中
- 第二步给RecycleView指定一个布局行为
修改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_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<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"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
...
</android.support.design.widget.CoordinatorLayout>
...
</android.support.v4.widget.DrawerLayout>
可以看到,首先定义一个AppBarLayout,并将Toolbar放置在了AppBarLayout里面,然后在RecycleView中使用app:layout_behavior属性,其中appbar_scrolling_view_behavior这个字符串由Design Support库提供。
当AppBarLayout接收到滚动事件的时候,它内部的子控件其实是可以指定如何去影响这些事件的。通过app:layout_scrollFlags="scroll|enterAlways|snap" 属性就能实现。
- scroll:表示当RecycleView向上滚动的时候,Toolbar会跟着一起向上滚动并实现隐藏。
- enterAlways:表示当RecycleView向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示。
- snap:表示当Toolbar还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示。
效果如下:
9、SwipeRefreshLayout 下拉刷新
SwipeRefreshLayout 由support-v4库提供。修改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_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
...
</android.support.design.widget.CoordinatorLayout>
...
</android.support.v4.widget.DrawerLayout>
在RecycleView的外面又嵌套了一成SwipeRefreshLayout,这样RecycleView就自动拥有下拉刷新的功能了。
另外注意:由于RecycleView现在变成了SwipeRefreshLayout的子控件,因此之前使用app:layout_behavior声明的布局行为现在也要移到SwipeRefreshLayout中才行。
修改MainActivity:
public class MainActivity extends AppCompatActivity {
private SwipeRefreshLayout swipeRefreshLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh);
swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent);
swipeRefreshLayout.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();
//表示刷新事件结束,并隐藏刷新进度条
swipeRefreshLayout.setRefreshing(false);
}
});
}
}).start();
}
...
}
setColorSchemeResources()方法来设置下拉刷新进度条的颜色。
setOnRefreshListener()方法设置下拉刷新的监听器。
swipeRefreshLayout.setRefreshing(false) 表示刷新事件结束,并隐藏刷新进度条。
效果如下:
10、CollapingToolbarLayout 可折叠式标题栏
CollapingToolbarLayout是一个作用于Toolbar基础之上的布局,它也是由Design Support库提供的。CollapingToolbarLayout可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,而是能够实现非常华丽的效果。
CollapingToolbarLayout 是不能独立存在的,它只能作为AppBarLayout的直接子布局。而AppBarLayout又必须是CoordinatorLayout的子布局。
这里通过水果的详情页面作为展示。新建FruitActivity,布局为activity_fruit.xml。
整个布局采用分段方式编写,分为两个部分,一个是水果的标题栏,另一个是水果内容详情。
- 首先实现标题栏部分,使用CoordinatorLayout 作为最外层布局。
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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="match_parent">
</android.support.design.widget.CoordinatorLayout>
- 接着在NestedScrollView中嵌套一个AppBarLayout
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
- 接下来在AppBarLayout中再嵌套一个CollapsingToolbarLayout
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
<android.support.design.widget.CollapsingToolbarLayout
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.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
app:contentScrim属性用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色,其实CollapsingToolbarLayout在折叠之后就是一个普通的Toolbar,那么背景肯定应该是colorPrimary了。
app:layout_scrollFlags属性之前见过,只不过之前是给Toolbar指定的,现在移到外面了。其中scroll表示CollapsingToolbarLayout会随着水果内容详情的滚动一起滚动; exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
4 . 接下来,在CollapsingToolbarLayout中定义标题栏的具体内容
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
<android.support.design.widget.CollapsingToolbarLayout
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">
<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" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
app:layout_collapseMode用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式。其中ImageView指定成 parallax,表示会在折叠的过程中产生一定的错位偏移,这种视觉效果比较好;Toolbar指定成pin,表示在折叠过程中位置始终保持不变。
5 . 这样水果的标题栏就编写完成了,下面开始编写水果的内容详情部分,继续修改activity_fruit.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
...
</android.support.design.widget.AppBarLayout>
<android.support.v4.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">
<android.support.v7.widget.CardView
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">
<TextView
android:id="@+id/fruit_content_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</android.support.v7.widget.CardView>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_comment"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end" />
</android.support.design.widget.CoordinatorLayout>
水果详情页的最外层使用了一个NestedScrollView,注意它和AppBarLayout是平级的。
NestedScrollView是在ScrollView基础上增加了响应滚动事件的功能。由于CoordinatorLayout本身可以响应滚动事件了,因此在它的内部就需要使用RecycleView或者NestedScrollView这样的布局。另外,这里通过app:layout_behavior属性指定了一个布局行为,这和之前在RecycleView中的用法一样。
最后还加入了一个FloatingActionButton,它和AppBarLayout、
NestedScrollView是平级的。app:layout_anchor="@id/appBar"属性指定一个锚点,这样悬浮按钮就会出现在水果标题栏的区域里。
现在加入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();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
最后修改FruitAdapter
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
...
@Override
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 v) {
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;
}
...
}
当点击首页水果的图片时,就会跳转到详情页。详情页的效果如下:
11、充分利用系统状态栏空间
想要让背景图能够和系统状态栏融合,需要借助android:fitsSystemWindows属性来实现。
在CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout这种嵌套结构布局中,将控件的android:fitsSystemWindows属性指定为true
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fitsSystemWindows="true">
<android.support.design.widget.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="?attr/colorPrimary"
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" />
...
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
...
</android.support.design.widget.CoordinatorLayout>
只是设置了android:fitsSystemWindows属性还是不行的,还必须在程序的主题中将状态栏颜色指定成透明色才行。为了兼容5.0以下版本,需要这么做:
在res下新建values-v21目录,然后再创建styles.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FruitActivityTheme" parent="AppTheme">
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
这里定义了一个FruitActivityTheme主题,然后在FruitActivityTheme中将状态栏颜色指定成透明色。由于values-v21目录是Android5.0 及以上系统才会读取的,因此这么声明是没问题的。
为了适配5.0以下版本,还需要在values/styles.xml文件进行修改。
<resources>
...
<style name="FruitActivityTheme" parent="AppTheme">
</style>
</resources>
可以看到,这里也定义了FruitActivityTheme主题,并且parent主题也是AppTheme,但是它的内部是空的。因为5.0之前的系统无法指定系统状态栏颜色,因此这里什么都不用做就可以了。
最后,在AndroidManifest.xml文件中修改FruitActivity,让其使用这个主题才行。
<activity
android:name=".FruitActivity"
android:theme="@style/FruitActivityTheme"/>