同一个产品经理出品的App, Android端的界面往往比不上iOS端. Android端界面整体色彩渲染的不协调, 一直受广大用户的诟病. Material Design的横空出世, 让Android端的使用者看到了希望--声称设计史上第一次超越了iOS端(作为一位iOS开发者,对此表示呵呵). 下面是Material Design实现的效果.
侧面抽屉效果的弹出, 以及详情界面顶部toolBar向上滚动时的渐变, 效果有没有很赞? 下面看一下具体实现.
首先, 需要在app的build.gradle文件中添加Material Design的支持库design, 其余的circleimageview, recyclerview, cardview, glide库, 项目中都会用到.
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:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:design:26.+'
compile 'de.hdodenhof:circleimageview:2.1.0'
compile 'com.android.support:recyclerview-v7:26.+'
compile 'com.android.support:cardview-v7:26.+'
compile 'com.github.bumptech.glide:glide:4.0.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.0.0'
testCompile 'junit:junit:4.12'
}
添加Toolbar
首先是将ActionBar替换成Toolbar,因为Toolbar才具有Materail Design效果. 修改路径app/src/main/res/values下的styles文件.
<resources>
//主题是淡色, 陪衬设置成深色;
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
//主题是身色, 陪衬设置成淡色;
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="FruitActivityTheme" parent="AppTheme"></style>
</resources>
将样式改为Light.NoActionBar后,顶部的ActionBar就会被去掉. 然后在activity_main布局中添加Toolbar.
<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"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Fruits"
app:layout_scrollFlags="scroll|enterAlways|snap"
></android.support.v7.widget.Toolbar>
完成了以上工作, 就可以在MainActivity中使用了.
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null){
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.menu);
}
setDisplayHomeAsUpEnabled方法用来控制Toolbar顶部右边是否显示返回按钮, setHomeAsUpIndicator方法可以给Toolbar设置图片.
Toobar右边的按钮又是怎么添加的呢? res目录下新建Directory, 命名为menu, 在该文件下新建文件toolbar.
<?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:title="Backup"
android:icon="@drawable/left"
app:showAsAction="always"
android:visible="true"
android:enabled="true"/>
<item
android:id="@+id/delete"
android:title="Delete"
android:icon="@drawable/right"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/settings"
android:title="settings"
android:icon="@drawable/left"
app:showAsAction="never"/>
</menu>
新建的布局包含了backup, delete, settings三个按钮. 其中showAsAction属性分别设置成always, ifRoom, never. 属性名称已经表明了界面效果.然后在MainActivity中的onCreateOptionsMenu方法中使用.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.toolbar, menu);
return true;
}
侧拉抽屉效果
该效果可以通过Material Design中的DrawerLayout实现.
<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"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Fruits"
app:layout_scrollFlags="scroll|enterAlways|snap"
></android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<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"
app:layout_behavior="@string/appbar_scrolling_view_behavior"></android.support.v7.widget.RecyclerView>
</android.support.v4.widget.SwipeRefreshLayout>
<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/newmsg"
android:foregroundTint="#000"/>
</android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#abc"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"
android:layout_gravity="start"
>
</android.support.design.widget.NavigationView>
</android.support.v4.widget.DrawerLayout>
DrawerLayout底层封装了一系列手势识别的方法. 使用时内部接收两个控件, 第一个CoordinatorLayout是主页面, 第二个NavigationView是侧滑时出现的页面.
CoordinatorLayout类似FrameLayout, 其内部的控件都会以父布局的左上角为参照. 不同的是CoordinatorLayout符合Material Design的设计理念, 可以自动识别CoordinatorLayout类型的控件并对它们进行有效调整, 提供更好的用户体验. 上面的例子中, 因为AppBarLayout和FloatingActionButton都是CoordinatorLayout的子类, 可以保证Toolbar不被SwipeRefreshLayout遮挡, FloatingActionButton也不会随着RecyclerView的上下滚动而出现偏移. NavigationView是Material Design抽屉效果中推荐的侧拉界面, 用于显示详情信息. 上面将名称为nav_menu和nav_header的布局文件分别放在了布局中的menu和layout文件夹下, 具体实现如下.
<?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/left"
android:title="call"/>
<item
android:id="@+id/nav_friends"
android:icon="@drawable/right"
android:title="friends"/>
<item
android:id="@+id/nav_location"
android:icon="@drawable/left"
android:title="location"/>
<item
android:id="@+id/nav_mail"
android:icon="@drawable/right"
android:title="mail"/>
</group>
</menu>
<?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:padding="10dp"
android:background="?attr/colorPrimary">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/icon_image"
android:layout_width="70dp"
android:layout_height="70dp"
android:src="@drawable/aa"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/mail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="Dog's mail: dog10@163.com"
android:textColor="#fff"
android:textSize="14sp"/>
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/mail"
android:text="Dog Yellow"
android:textColor="#fff"
android:textSize="14sp"/>
</RelativeLayout>
完成以上布局, 就可以在MainActivity中处理相关逻辑了.
public class MainActivity extends AppCompatActivity {
private DrawerLayout drawerLayout;
private SwipeRefreshLayout swipeRefreshLayout;
private Fruit[] fruits = {
new Fruit("Apple", R.drawable.apple),
new Fruit("banana", R.drawable.banana),
new Fruit("berray", R.drawable.berray),
new Fruit("tomato", R.drawable.tomato),
};
private List<Fruit> fruitList = new ArrayList<>();
private FruitAdapter adapter;
@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.colorPrimary);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
refreshFruits();
}
});
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
NavigationView navigationView = (NavigationView)findViewById(R.id.nav_view);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null){
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.menu);
}
navigationView.setCheckedItem(R.id.nav_call);
navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener(){
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
drawerLayout.closeDrawers();
return true;
}
});
FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Toast.makeText(MainActivity.this, "fabs clicked!", Toast.LENGTH_SHORT).show();
Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT).setAction("Undo", new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "Data restore", Toast.LENGTH_SHORT).show();
}
}).show();
}
});
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);
}
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();
}
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]);
}
}
//menu的item最多显示2个,属性设置为never时,被折叠不显示.展开的大小是固定的.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.toolbar, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case android.R.id.home:
drawerLayout.openDrawer(GravityCompat.START);
break;
case R.id.backup:
Toast.makeText(this, "Back up", Toast.LENGTH_SHORT).show();
break;
case R.id.delete:
Toast.makeText(this, "Delete", Toast.LENGTH_SHORT).show();
break;
case R.id.settings:
Toast.makeText(this, "Setting", Toast.LENGTH_SHORT).show();
break;
}
return true;
}
}
其中Fruit实体类和FruitAdapter是RecyclerView正常显示所需的java文件. 不明白的可以参照:ListView和RecyclerView
public class Fruit {
private String name;
private int imageId;
public Fruit(String apple, int imageId) {
this.name = apple;
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;
}
}
public class FruitAdapter extends RecyclerView.Adapter <FruitAdapter.ViewHolder>{
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_image);
fruitName = (TextView)view.findViewById(R.id.fruit_name);
}
}
private List<Fruit> mFruits;
private Context mContext;
public FruitAdapter(List<Fruit> fruits){
mFruits = fruits;
}
@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 view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruits.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;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Fruit fruit = mFruits.get(position);
holder.fruitName.setText(fruit.getName());
Glide.with(mContext).load(fruit.getImageId()).into(holder.fruitImage);
}
@Override
public int getItemCount() {
return mFruits.size();
}
}
RecycleView中的单元控件使用了Material Design中的CardView, 包含ImageView和TextView两个控件.
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_margin="5dp"
app:cardCornerRadius="4dp">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<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="12sp"
/>
</LinearLayout>
</android.support.v7.widget.CardView>
设置CardView中的Image时使用了Glide, 可以结合设备屏幕分辨率对图片的清晰度自动调整, 保证相对较好的渲染效果. 使用前需要导入相应的库, 具体设置可以参照文章开头.
Fruit详情页面
在FruitAdapter中给ViewHolder添加点击方法, 点击CardView的item后, 跳转到FruitActivity页面, 其布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true"
>
<ImageView
android:id="@+id/fruit_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:fitsSystemWindows="true"/>
<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.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<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"></android.support.v7.widget.CardView>
<TextView
android:id="@+id/fruit_content_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"/>
</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/left"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>
</android.support.design.widget.CoordinatorLayout>
该页面包含3部分: 上面的AppBarLayout, 下面的NestedScrollView, 以及悬浮在页面的FloatingActionButton. 最后的FloatingActionButton的锚点依托在AppBarLayout的右下部.
使用CollapsingToolbarLayout可以实现随着ScrollView滚动的效果. 设置为layout_scrollFlags属性为'scroll|exitUntilCollapsed'后, CollapsingToolbarLayout就能根据ScrollView向上滚动的距离来决定内部控件的显示或者隐藏.为了满足Material Design设计, 需要将布局放在AppBarLayout当中.
Material Design当中允许修改顶部状态栏. 首先将符合CoordinatorLayout的父布局和子布局的fitsSystemWindows属性全部设置成true. 然后修改AppTheme为透明色. 由于Material Design是在Android5.0推出, 所以需要在res文件夹下新建目录values-v21对Android的不同版本进行适配. 在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>
NestedScrollView是符合Material Design标准的ScrollView, 使用方式也比较类似. 有了布局就可以在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 collapsingToolbarLayout = (CollapsingToolbarLayout)findViewById(R.id.collapsing_toolbar);
ImageView fruitImageView = (ImageView)findViewById(R.id.fruit_image_view);
TextView fruitContextText = (TextView)findViewById(R.id.fruit_content_text);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null){
actionBar.setDisplayHomeAsUpEnabled(true);
}
collapsingToolbarLayout.setTitle(fruitName);
Glide.with(this).load(fruitImageId).into(fruitImageView);
String fruitContent = generateFruitContent(fruitName);
fruitContextText.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);
}
}
总结
Material Design是Android为了增强UI设计效果和提高用户体验而推荐的标准, 背后是统一的设计理念, 为标准繁多的Android界面设计吹来了一股清风. 上面的例子中利用Material Design中推出的组件替换了App的ActionBar为Toolbar,利用DrawerLayout实现了抽屉效果,并且在Fruit的详情页面,利用CollapsingToolbarLayout实现了Toolbar随NestedScrollView滚动而变化. 相信经过以上实践, 能够对Material Design的理解能够更加深入. 也希望在App的开发中能够践行Material Design的设计理念.
喜欢和关注都是对我的支持和鼓励~