Android学习笔记之Material Design实战

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中如下所示:

<application

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:supportRtl="true"

android:theme=‘@style/AppTheme"

>

...

</application>

可以看到,这里使用android:theme属性指定了一个AppTheme的主题,那么这个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主题,我们之前所有的项目中自带的ActionBar就是因为指定了这个主题才出现的。

而现在我们准备使用Toolbar来替代ActionBar,因此需要指定一个不带ActionBar的主题,通常Theme.AppCompat.NoActionBar和ThemeAppCompat.Light.NoActionBar这两种主题可选,其中Theme.AppCompat.NoActionBar表示深色主题,它会将界面的主体颜色设成深色,陪衬颜色设成淡色。而Theme.AppCompat.Light.NoActionBar表示淡色主题,他会将界面的主体颜色设成淡色,陪衬颜色设成深色。具体效果你可以试一试,这里由于我们之前的程序一直都是以淡色为主的,那么我就选用淡色主题了了,如下所示:

<resources>

<!--Base application theme -->

<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>

然后观察一下AppTheme中的属性重写,这里重写了colorPrimary、colorPrimaryDark和colorAccent这3个属性的颜色。除上述3个属性之外,我们还可以通过textColorPrimary、windowBackground和navigationBarColor等属性来控制更多位置的颜色。不过唯独colorAccent这个属性比较难理解,它不只是用来指定这样一个按钮的颜色,而是更多表达了一个强调的意思,比如一些控件的选中状态也会使用这个颜色。

现在我们已经将ActionBar隐藏起来了,那么接下来看一看如何使用Toolbar来替代ActionBar。修改activity_material_design_test.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:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

</FrameLayout>


toolbar.xml代码如下:

<?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="@android:drawable/ic_menu_close_clear_cancel"
        android:title="Backup"
        app:showAsAction="always"
        />
    <item
        android:id="@+id/delete"
        android:icon="@android:drawable/ic_menu_delete"
        android:title="Delete"
        app:showAsAction="ifRoom"
        />
    <item
        android:id="@+id/settings"
        android:icon="@android:drawable/ic_menu_more"
        android:title="Settings"
        app:showAsAction="never"
        />
</menu>

使用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中代码如下:

<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:popupTheme="@style/ThemeOverlay.AppCompat.Light"

/>

</FrameLayout>

<TextView

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"

/>

</android.support.v4.widget.DrawerLayout>

可以看到,这里最外层的控件使用了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"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/nav_call"
            android:icon="@android:drawable/ic_menu_call"
            android:title="Call"
            />
        <item
            android:id="@+id/nav_friends"
            android:icon="@android:drawable/ic_menu_agenda"
            android:title="Friends"
            />
        <item
            android:id="@+id/nav_location"
            android:icon="@android:drawable/ic_menu_mylocation"
            android:title="Location"
            />
        <item
            android:id="@+id/nav_mail"
            android:icon="@android:drawable/ic_menu_report_image"
            android:title="Mail"
            />
        <item
            android:id="@+id/nav_task"
            android:icon="@android:drawable/ic_menu_recent_history"
            android:title="Tasks"
            />
    </group>
</menu>

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:padding="10dp"
    android:background="?attr/colorPrimary"
    >
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/icon_iamge"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:src="@mipmap/ic_launcher"
        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="tonygreen@gmail.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="Tony Green"
        android:textColor="#FFF"
        android:textSize="14sp"
        />
</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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawaer_layout"
    >
    <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:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        />

    </FrameLayout>
    <!--<TextView
        android:id="@+id/tv_menu"
        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"
        />-->
    <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:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"
        />
</android.support.v4.widget.DrawerLayout>

这里通过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"?>
<android.support.v4.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawaer_layout"
    >
    <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: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="@android:drawable/ic_menu_share"
            app:elevation="8dp"
            />

    </FrameLayout>
    </android.support.v4.widget.DrawerLayout>

这里使用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"?>
<android.support.v4.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawaer_layout"
    >
    <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: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="@android:drawable/ic_menu_share"
            app:elevation="8dp"
            />

    </android.support.design.widget.CoordinatorLayout>
    <!--<TextView
        android:id="@+id/tv_menu"
        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"
        />-->
    <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:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"
        />
</android.support.v4.widget.DrawerLayout>

由于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.support.v7.widget.CardView

android:layout_width=’match_parent"

android:layout_height="wrap_content"

app:cardCornerRadius=”4dp“

app:elevation="5dp"

>

<TextView

android:id="@+id.info_text"

android:layout_width="match_parent"

android:layout_height="wrap_content"

/>

</android.support.v7.widget.CardView>

这里定义了一个CardView布局,我们可以通过app;cardCornerRadius属性指定卡片圆角的弧度,数值远大,圆角的弧度也越大。另外还可以通过app:elevation属性指定卡片的高度,高度值越大,投影范围也越大,但是投影效果越淡,高度值越小,投影范围也越小,但是投影效果越浓。

添加波纹点击效果

默认情况,CardView是不可点击的,并且没有任何的触摸反馈效果。触摸反馈动画在用户点击CardView时可以给用户以视觉上的反馈。为了实现这种行为,你必须提供一下属性:

<android.support.v7.widget.CardView
  ...
  android:clickable="true"
  android:foreground="?android:attr/selectableItemBackground">
  ...
</android.support.v7.widget.CardView>

使用android:foreground=”?android:attr/selectableItemBackground”可以使CardView点击产生波纹的效果,有触摸点向外扩散。

常见属性

cardBackgroundColor -----》 设置cardview背景颜色

cardCornerRadius -----》设置cardview 圆角的半径

cardElevation----》设置cardview的阴影

contentPadding-----》设置cardview的内间距

5.x上面可以设置点击水波纹效果

android:foreground="?attr/selectableItemBackground"

cardPreventCornerOverlap ------》防止api20之前圆角与内容重叠 ,在21以后不会重叠

cardUseCompatPadding ----》设置为true 可以为不同L版提供内间距兼容 ,默认为false

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'
}

这里添加了一个Glide库的依赖。Glide是一个超级强大的图片加载库,它不仅可以用于加载本地图片,还可以加载网络图片、GIF图片、甚至是本地视频。最重要的是,Glide的用法非常简单,只需一行代码就能轻松实现复杂的图片加载功能。Glide的项目主页地址是:https://github.com/bumptech.glide。

主布局代码:
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawaer_layout"
    >
    <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: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="@android:drawable/ic_menu_share"
            app:elevation="8dp"
            />

    </android.support.design.widget.CoordinatorLayout>
    <!--<TextView
        android:id="@+id/tv_menu"
        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"
        />-->
    <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:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"
        />
</android.support.v4.widget.DrawerLayout>
定义一个实体类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 void setName(String name) {
        this.name = name;
    }

    public int getImageId() {
        return imageId;
    }

    public void setImageId(int imageId) {
        this.imageId = imageId;
    }
}
RecyclerView子项布局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_iamge"
            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>
这里使用了CardView来作为子项的最外层布局,CardView由于是一个FrameLayout,因此没有什么方便的定位方式,这里我们只好再其中再嵌入一个LinearLayout,然后在里面再放置具体内容。


为RecyclerView准备一个适配器,新建FruitAdapter类,代码如下所示:

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {

    private Context mContext;

    private List<Fruit> 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<Fruit> 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 List<Fruit> fruitList = 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"?>
<android.support.v4.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawaer_layout"
    >
    <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:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                app:layout_scrollFlags="scroll|enterAlways|snap"
                />
        </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.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="@android:drawable/ic_menu_share"
            app:elevation="8dp"
            />

    </android.support.design.widget.CoordinatorLayout>
    <!--<TextView
        android:id="@+id/tv_menu"
        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"
        />-->
    <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:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"
        />
</android.support.v4.widget.DrawerLayout>

可以看到,布局文件并没有太大的变化,我们首先定义了一个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"?>
<android.support.v4.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawaer_layout"
    >
    <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:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                app:layout_scrollFlags="scroll|enterAlways|snap"
                />
        </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"
                />
        </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="@android:drawable/ic_menu_share"
            app:elevation="8dp"
            />

    </android.support.design.widget.CoordinatorLayout>
    <!--<TextView
        android:id="@+id/tv_menu"
        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"
        />-->
    <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:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"
        />
</android.support.v4.widget.DrawerLayout>

这里我们在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 List<Fruit> fruitList = 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"?>
<android.support.design.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"
    tools:context="com.eastelsoft.nuonuo.activity.FruitActivity">
    <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:layout_width="match_parent"
                android:layout_height="match_parent"
                android:id="@+id/fruit_image_view"
                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.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: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"
                >
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/fruit_content_text"
                    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="@android:drawable/ic_menu_share"
        app:layout_anchor="@id/appBar"
        app:layout_anchorGravity="bottom|end"
        />
</android.support.design.widget.CoordinatorLayout>

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"?>
<android.support.design.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"
tools:context="com.eastelsoft.nuonuo.activity.FruitActivity"
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: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.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: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"
            >
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/fruit_content_text"
                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="@android:drawable/ic_menu_share"
    app:layout_anchor="@id/appBar"
    app:layout_anchorGravity="bottom|end"
    />
</android.support.design.widget.CoordinatorLayout>
但是,即使我们将android:fitsSystemWindows属性都设置好了还是没有用的,因为还必须在程序的主题中将状态栏颜色指定成透明色才行。指定成透明色的方法很简单,在主题中将android:statusBarColor这个属性的值指定成@android:color/transparent就可以了。但问题在于,android:statusBarColor这个属性是从API21,也就是Android5.0系统开始才有的,之前的系统无法指定这个属性,。那么,系统差异型的功能实现就要从这里开始了。
在res中创建values-21目录,创建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>
但是,Android5.0之前的系统却无法识别FruitActivityTheme这个主题,因此我们还需要对values/styles.xml文件进行修改,如下所示:
<resources>
    <!-- Base application theme. -->
    <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>

    <style name="FruitActivityTheme" parent="AppTheme">
    </style>
</resources>
可以看到,这里我们也定义了一个FruitActivityTheme主题,并且parent主题也是AppTheme,但是它的内部是空的。因为Android5.0之前的系统无法指定状态栏的颜色,因此这里什么都不做就可以了。
最后,我们还需要让FruitActivity使用这个主题才可以,修改AndroidManifest.xml中的代码,如下所示:
<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。

另附两篇博文学习:
12个Material Design风格控件的使用https://blog.csdn.net/hebin320320/article/details/51461611
AndroidMaterialDesign(一)史上最全的材料设计控件大全       https://blog.csdn.net/johnny901114/article/details/51918436


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值