Android 详解 Material Design

1. 什么是 Material Design?

答:

  • 定义:Material DesignGoogle 在 推出的一套全新的界面设计语言。它基于传统优秀的设计原则、结合丰富的创意和科技,包含了视觉、运动、交互效果等特性。
  • 发展:

    • 2014Google I/O 大会, Google 推出 Material Design
    • Android 5.0 系统开始,Google 将所有内置应用都使用 Material Design 风格进行设计。
    • 2015Google I/O 大会,Google 推出了一个 Design Support 库,这个库将 Material Design 中最具代表性的一些控件和效果进行了封装,使得开发者可以轻松地将自己的应用 Material 化。
  • 官方文章:https://material.google.com

2. 怎样理解 Toolbar?

答:

  • ActionBarToolbar 的对比?

    • 每个活动最顶部的那个标题栏就是 ActionBar由于自身设计的原因,ActionBar 只能位于活动的顶部,不能实现一些 Material Design 的效果。官方不再建议使用 ActionBar,而是更加推荐使用 Toolbar
    • Toolbar 不仅继承了 ActionBar 的所有功能,而且灵活性很高,和可以配合其他控件来完成一些 Material Design 的效果。
    • Fragment 中最好不要直接使用 Toolbar 或者 ActionBar,复用的时候可能会出现意想不到的效果。
  • 主题 Theme.AppCompat.NoActionBarTheme.AppCompat.Light.NoActionBar 的区别?

    • AndroidManifest.xml 文件的 application 标签下,有一个 theme 属性,这个属性指定了一个 AppTheme 的主题,资源文件中这个 AppTheme 主题的 parent 默认是 Theme.AppCompat.Light.DarkActionBar
    • Theme.AppCompat.NoActionBar:表示深色主题,它会将界面的主题颜色设为深色,陪衬颜色设为淡色。
    • Theme.AppCompt.Light.NoActionBar:表示淡色主题,它会将界面的主题颜色设为淡色,陪衬颜色设为深色。
  • AppTheme 中的几个属性分别什么意思?

    • colorPrimary:应用标题栏的颜色。
    • colorPrimaryDark:系统状态栏的颜色。
    • textColorPrimary:应用标题栏中文字的颜色。
    • windowBackground:应用界面背景颜色。
    • navigationBarColor:应用底部导航栏颜色。
    • `colorAccent:不只是用来指定一个按钮的颜色(比如FAB),更多表达一个**强调**的意思,比如一些控件的选中状态也会使用colorAccent` 的颜色。
  • 解释一下 activity_main.xml 布局文件中 Toobar 标签主要属性的作用?

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    
        // 布局文件中指定一个 xmlns:app 的命名空间是因为:
        // Material Design 是在 Android 5.0 系统中才出现的,很多的 Material 属性在 5.0 之前的系统中并不存在
        // 为了兼容之前的老系统,就不能使用 android:attribute 这样的写法了,而是应该使用 app:attribute
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <android.support.v7.widget.Toolbar // Toolbar 控件是 appcompat-v7 库提供的
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize" // 高度设置为 actionBar 的高度
            android:background="?arrr/colorPrimary" // 背景色
    
            // 前提是在 style.xml 中将程序的主题指定了淡色主题
            // 所以 Toolbar 上面的各种元素就会自动使用深色系,这是为了和主体颜色区分开
            // 但是这个效果很差,之前使用 ActionBar 时文字都是白色的,现在变成黑色会很难看
            // 为了能让 Toolbar 单独使用深色主题,而不受 styles.xml 中程序主题影响,这里使用 android:theme 属性进行单独制定
            // 但这也单独制定后又有新问题,如果 Toolbar 中有菜单项,那么弹出的菜单项也会变成深色主题,这样就再次变得很难看
            // 所以这里使用了 app:popupTheme 属性,将单独弹出的菜单项指定成了淡色主题
            android:theme="@style/ThemeOverlay.AppCompat.DarkActionBar" // 即,影响标题栏内文字的颜色
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> // 即,影响标题栏内菜单项的颜色
    </FrameLayout>
    
    // 使用 Toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    
  • 解释一下下面 action 按钮布局文件 toolbar.xmlitem 标签主要属性的作用?

    <menu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"> // 使用 app 命名空间兼容低版本系统
        <item // 使用 <item> 标签定义 action 按钮,Toolbar 中的 action 按钮只会显示图标、菜单中的 action 按钮只会显示文字
            android:id="@+id/backup"
            android:icon="@drawable/ic_backup"
            android:title="Backup"
            app:showAsAction="always" /> // always 表示永远显示在 Toolbar 中,如果屏幕空间不够则不显示
    
        <item // 使用 <item> 标签定义 action 按钮,Toolbar 中的 action 按钮只会显示图标、菜单中的 action 按钮只会显示文字
            android:id="@+id/delete"
            android:icon="@drawable/ic_delete"
            android:title="Delete"
            app:showAsAction="ifRoom" /> // ifRoom 表示屏幕空间足够的情况下显示在 Toolbar 中,不够的话就显示在菜单中
    
        <item // 使用 <item> 标签定义 action 按钮,Toolbar 中的 action 按钮只会显示图标、菜单中的 action 按钮只会显示文字
            android:id="@+id/settings"
            android:icon="@drawable/ic_settings"
            android:title="Settings"
            app:showAsAction="never" /> // never 表示不显示在 Toolbar 中,只显示在菜单中
    </menu>
    
    // 使用 toolbar.xml
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.toolbar, menu);
        return true;
    }
    

3. 使用 DrawerLayout 和 NavigationView 实现一个滑动菜单 Demo?

答:

  • MainActivity

    public class MainActivity extends AppCompatActivity {
        private Toolbar mToolbar;
        private DrawerLayout mDrawerLayout;
        private NavigationView mNavView; // 使用 NavigationView 需要在 app/build.gradle 文件的 dependencies 闭包中添加依赖
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mToolbar = (Toolbar) findViewById(R.id.toolbar);
            mDrawableLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
            mNavView = (NavigationView) findVieById(R.id.nav_view);
    
            ActionBar actionBar = getSupportActionBar(); // 这里这个 ActionBar 实例的具体实现是由 Toolbar 来完成的
            if(actionBar != null) { // 在 Toolbar 的最左边加上一个导航按钮,实际上这个按钮叫作 HomeAsUp 按钮,默认图标是一个返回的箭头
                actionBar.setDisplayHomeAsUpEnabled(true); // 让导航按钮显示出来
                actionBar.setHomeAsUpIndicator(R.drawable.ic_menu); // 设置导航按钮图标
            }
    
            mNavView.setCheckedItem(R.id.nav_call); // 将 Call 菜单项设置为默认选中
            mNavView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { // 设置菜单项选中的监听器
                @Override
                public boolean onNavigationItemSelected(MenuItem item) {
                    mDrawerLayout.closeDrawers(); // 将滑动菜单关闭
                    return true;
                }
            });
        }
    
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            switch (item.getItemId()) {
                case android.R.id.home: // HomeAsUp 按钮的 id 永远都是 android.R.id.home
                    // 将滑动菜单展示出来,方法要求传入一个 Gravity 参数,这里为了保证行为和 XML 中定义的一致,传入 GravityCompat.START
                    mDrawerLayout.openDrawer(GravityCompat.START);
                    break;
                default:
                    break;
            }
            return true;
        }
    }
    
  • 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: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"
    
            // 这个属性必须要指定,我们需要告诉 DrawerLayout 滑动菜单是在屏幕的左边还是右边
            // 指定 left 表示滑动菜单在左边,指定 right 表示滑动菜单在右边
            // 指定 start 表示会根据系统语言进行判断,如果系统语言是从左往右的,比如英语、汉语,滑动菜单就在左边;如果系统语言是从右往左的,比如阿拉伯语,滑动菜单就在右边
            android:layout_gravity="start"
    
            // 在 nav_menu.xml 布局文件标签 <menu> 中嵌套了一个 <group> 标签,然后将 group 的 checkableBehavior 属性指定为 single
            // group 表示一个组,checkableBehavior 指定为 single 表示组中所有菜单项只能单选
            app:menu="@menu/nav_menu"
            app:headerLayout="@layout/nav_header" /> // 将 nav_header.xml 文件的高度设为 180dp,这是一个 NavigationView 比较适合的高度
    
    </android.support.v4.widget.DrawerLayout>
    

4. View 的 elevation 属性的作用是?

答:elevation 属性可以给 View 指定一个高度值,高度值越大,投影范围也越大,但是投影效果越淡;高度值越小,投影范围也越小,但投影效果越浓FloatingActionButton 中有这个属性的默认值,可以看到投影/阴影效果。

5. Snackbar 和 Toast 的区别?

答:Snackbar 不是 Toast 的替代品,它们两者之间有着不同的应用场景。

  • Toast:告诉用户现在发生了什么事情,但同时用户只能被动接收这个事情,用户不能做出选择。
  • Snackbar:对交互逻辑进行了扩展,不管是出现还是消失都是带有动画效果的,而且允许在提示当中加入一个可交互按钮,当用户点击按钮的时候可以执行一些额外的逻辑操作。

6. 使用 Snackbar 实现一个简单的 Demo?

答:

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // 第一个参数需要传入一个 View,只要是当前页面布局的任意一个 View 都可以,Snackbar 会使用这个 View 来自动查找最外层的布局,用于展示 Snackbar
        // 第二个参数是 Snackbar 中显示的内容
        // 第三个参数是 Snackbar 的显示时长
        Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT)
                .setAction("Undo", new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(MainActivity.this, "Data restored", Toast.LENGTH_SHORT).show();
                    }
                })
                .show();
    }
});

7. 怎样理解 CoordinatorLayout?

答:

  • CoordinatorLayout 是一个加强版的 FrameLayout
  • CoordinatorLayout 可以监听其所有子控件的各种事件,然后自动帮我们做出最为合理的响应,比如防止子控件之间的遮挡、子控件之间动画同步等

8. 怎样理解 CardView?

答:

  • CardView 是用于实现卡片式布局效果的重要控件,由 appcompat-v7 提供。
  • CardView 也是一个 FrameLayout,只是额外提供了圆角阴影等效果。

9. 怎样理解 AppBarLayout?

答:

  • AppBarLayout 实际上是一个垂直方向的 LinearLayout
  • AppBarLayout 在内部做了很多滚动事件的封装,并应用了一些 Material Design 的设计理念

10. 使用 CoordinatorLayout 和 AppBarLayout 等实现一个滚动流畅的 Demo?

答:

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

                // AppBarLayout 接收到滚动事件的时候,内部的子控件可以指定如何去影响这些事件
                // scroll 表示当 RecyclerView 向上滚动的时候,Toolbar 会跟着一起向上滚动并实现隐藏
                // enterAlways 表示当 RecyclerView 向下滚动的时候,Toolbar 会跟着一起向下滚动并重新显示
                // snap 表示当 Toolbar 还没有完全隐藏或显示的时候,会根据当前滚动的距离自动选择是隐藏还是显示
                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"

            // RecyclerView 滚动的时候将滚动事件通知给 AppBarLayout
            // 如果 RecyclerView 外面嵌套的有父布局,比如 SwipeRefreshLayout,则 app:layout_behavior 声明的布局行为也要移到父布局中才行
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />

            ...

    </android.support.design.widget.CoordinatorLayout>

    ...

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

11. 怎样理解 SwipeRefreshLayout?

答:

  • SwipeRefreshLayout 是用于实现下拉刷新功能的核心类,由 support-v4 提供。
  • 在布局文件中,如果在 RecyclerView 的外面嵌套了一层 SwipeRefreshLayout,这样 RecyclerView 就自动拥有下拉刷新功能了。
  • 需要注意的是,由于 RecyclerView 现在变成了 SwipeRefreshLayout 的子控件,因此之前使用 app:layout_behavior 声明的布局行为现在也要移到 SwipeRefreshLayout 中才行。
  • Demo

    public class MainActivity extends AppCompatActivity {
        private SwipeRefreshLayout mSwipeRefresh;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mSwipeRefresh = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh);
            mSwipeRefresh.setColorSchemeResources(R.color.colorPrimary); // 设置下拉刷新进度条颜色
            // 设置下拉刷新监听器
            mSwipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
                @Override
                pubic 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();
        }
    }
    

12. 怎样理解 CollapsingToolbarLayout?

答:

  • CollapsingToolbarLayout 是一个作用于 Toolbar 基础之上的布局,也是由 Design Support 库提供的,可以实现可折叠式标题栏的效果。
  • CollapsingToolbarLayout 是不能独立存在的,它在设计的时候就被限定只能作为 AppBarLayout 的直接子布局来使用,而 AppBarLayout 又必须是 CoordinatorLayout 的子布局
  • Demo

    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto" // Material Design 开发时会用到 xmlns:app 命名空间
        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" // 高度指定为 250dp,视觉效果较好
            android:fitsSystemWindows="true" >
    
            // 调用 CollapsingToolbarLayout 的 setTitle() 方法设置页面标题
            <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" // 这里要实现更加高级的 Toolbar 效果,因此需要将这个主题的指定从 Toolbar 提到上一层来
                android:fitsSystemWindows="true"
                app:contentScrim="?attr/colorPrimary" // 指定 CollapsingToolbarLayout 在趋于折叠状态以及折叠之后的背景色
    
                // 同上,也从 Toolbar 中移到上一层
                // scroll 表示 CollapsingToolbarLayout 会随着内容详情的滚动一起滚动
                // exitUntilCollapsed 表示当 CollapsingToolbarLayout 随着滚动完成折叠之后就保留在页面上,不再移除屏幕
                app:layout_scrollFlags="scroll|exitUntilCollapsed" >
    
                <ImageView
                    android:id="@+id/fruit_image_view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="centerCrop"
                    android:fitsSystemWindows="true" // 表示会为系统状态栏留出空间
    
                    // 用于指定当前控件在 CollapsingToolbarLayout 折叠过程中的折叠模式
                    app:layout_collapseMode="parallax" /> // parallax 表示会在折叠的过程中产生一定的错位偏移,这种模式的视觉效果会很好
    
                <android.support.v7.widget.Toolbar
                    android:id="@+id/toolbar"
                    android:layout_width="match_parent"
                    android:layout_height="?attr/actionBarSize"
                    app:layout_collapseMode="pin" /> // pin 表示在折叠过程中位置始终保持不变
            </android.support.design.widget.CollapsingToolbarLayout>
    
        </android.support.design.widget.AppBarLayout>
    
        // 内容详情部分
        // NestedScrollView 在 ScrollView 的基础之上增加了嵌套响应滚动事件的功能
        // 由于 CoordinatorLayout 本身已经可以响应滚动事件了,因此在它的内部就需要使用 NestedScrollView 或 RecyclerView 这样的布局
        // 不管是 ScrollView 还是 NestedScrollView,它们的内部都只允许存在一个直接子布局,否则 AS 会有提示
        <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_heigh="match_parent"
                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" // 圆角
                    app:elevation="5dp" > // 阴影
    
                    <TextView
                        android:id="@+id/fruit_content_text"
                        android:layout_width="wrap_content"
                        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" // 指定将 AppBarLayout 作为一个锚点,这样悬浮按钮就会出现在标题栏的区域内
            app:layout_anchorGravity="bottom|end" /> // 将悬浮按钮定位在标题栏区域的右下角
    
    </android.support.design.widget.CoordinatorLayout>
    

13. 怎样实现沉浸式状态栏?

答:Android 5.0 系统(API 21)之后支持沉浸式系统状态栏,有两种实现方式:

  • 方式一(xml 文件实现):

    • 第一步:必须将 ImageView 布局结构以及所有父布局都设置 fitsSystemWindows 属性为 true
    • 第二步:必须在应用的主题中将状态栏颜色指定成透明色。可以创建一个 values-21 目录,目录下创建一个 style.xml 文件,对属性 android:statusBarColor 的值指定成 @android:color/transparent 即可。因为 values-21 目录是只有 Android 5.0 即以上的系统才会去读取,所以这么声明式没有问题的。
    • 第三部,因为 Android 5.0 之前的系统无法识别新建的主题,所以对原主题文件进行修改,把 <style> 标签内去掉 android:statusBarColor 属性的指定,然后对 Activity 应用这个主题就可以了。
  • 方式二(代码实现):

    public class WeatherActivity extends AppCompatActivity {
        ...
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            if(Build.VERSION.SDK_INT >= 21) {
                View decorView = getWindow().getDecorView();
                // 表示活动的布局会显示在状态栏上面
                decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
                // 设置状态栏为透明色
                getWindow().setStatusBarColor(Color.TRANSPARENT);
            }
            setContentView(R.layout.activity_weather);
            ...
        }
        ...
    }
    
-------------本文结束感谢您的阅读-------------