Material Design实战

1、Toolbar

ActionBar由于其设计的原因, 被限定只能位于活动的顶部, 从而不能实现一些Material Design的效果, 因此官方现在已经不再建议使用ActionBar, 而是推荐使用ToolBar。ToolBar不仅继承了ActionBar的所有功能, 而且灵活性高, 可以配合其他控件来完成一些Material Design效果。 接下来添加一些action 按钮来让ToolBar 更丰富一些, 创建一个 menu文件夹 及toolbar.xml文件:




    

    

    

 可以看到, 此处通过标签来定义action按钮。app:showAsAction来指定按钮的显示位置, never表示永远显示在菜单中, always表示永远显示在toolbar。

在onCreateOptionsMenu()方法中加载toolbar.xml这个菜单文件, 在onOptionsItemSelected()方法中处理各个按钮的点击事件。

2、滑动菜单

(1)、DrawerLayout

DrawerLayout是一个布局, 在布局中允许放入两个直接子控件, 第一个子控件是主屏幕中显示的内容, 第二个子控件是滑动菜单中显示的内容。 修改主活动布局代码:


    
    
        
        
        

    


    
        
 

第一个子控件是FrameLayout, 用于显示主屏幕中显示的内容, 里面有我们刚定义的toolbar,第二个子控件TextView 其实是有什么都可以, DrawerLayout 并没有限制只能使用固定的控件。看看效果:

Material Design实战_第1张图片

另外我们可以添加导航浏览按钮, 点击也可以使菜单打开, 修改主活动代码:

public class MainActivity extends AppCompatActivity {

    private DrawerLayout drawer_layout;  //

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        android.support.v7.widget.Toolbar toolbar = (android.support.v7.widget.Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        drawer_layout = (DrawerLayout) findViewById(R.id.drawer_layout);//
        ActionBar actionBar = getSupportActionBar();  //
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);

        }


    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.toolbar, menu);

        return true;

    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){   //
            case R.id.home:
                drawer_layout.openDrawer(GravityCompat.START);
                break;

            case R.id.backup:
                Toast.makeText(this, "Backup",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;
                default:
        }
    return true;
    }

}

得到DrawerLayout的实例后调用 getSupportActionBar()方法得到了 ActionBar 的实例, 虽然这个ActionBar 实现是由Toolbar完成的 , 接着调用 ActionBar 的setDisplayHomeAsUpEnabled() 方法让导航按钮显示出来, 又调用了 setHomeAsUpIndicator() 方法来设置一个导航栏按钮图标。 实际上ToolBar 最左侧的这个按钮就叫做 HomeAsUp 按钮, 它的默认图标是一个返回的箭头, 这里将他的默认的样式和作用都修改了。

  接着对HomeAsUp按钮的点击事件进行处理, HomeAsUp按钮的id 永远都是android.R.id.home。 然后调用openDrawer() 方法将滑动菜单展示出来, 为了保证这里的行为和XML 中定义的一致, 传入GravityCompat.START。 

Material Design实战_第2张图片

(2)、NavigationView

NavigationView是 Design Support库中提供的一个控件, 它不仅严格按照Material Design 的要求来进行设计, 而且还可以讲话的菜单页面的实现变得非常简单。 应用前,首先将这个库引入到项目中:

 

dependencies {
 
    implementation 'com.android.support:design:26.1.0'
    implementation 'de.hdodenhof:circleimageview:2.1.0'

}

   第一行是 Design Support 库, 第二行是一个开源项目 CircleImageView ,它可以实现图片圆形化的功能。 在开始使用 NavigationView 之前, 需要准备好 menu 和 headerlayout。 menu用来在NavigationView显示具体菜单项 ,headerlayout 用来在NavigationView显示头部布局。

   先来准备menu, 在menu文件夹创建 nav_menu.xml文件:



    

        

        

        

        

        
     


中嵌套了一个 标签, 将checkableBehavior 属性指定为single。 group表示一个组, checkableBehavior 表示所有菜单项只能单选。

 menu就这样准备好了。。。接下来应该喜欢被headerLayout!!!我们就在headerLayout 中放置头像, 用户名, 邮箱地址这3项内容吧。 




    
    
    

    
 

   现在menu 和 headerLayout 都准备好了, NavigationView 随时准备着, 修改主活动布局:



    

        
        
    


                         //

  通过app:menu 和 app:headerLayout 属性将我们刚才准备好的menu 和 headeLayout 设置进去, 这样NavigationView 就这样定义完成了。 

   

  NavigationView 定义好了之后, 我们还需要去处理菜单项的点击事件才行。 修改主活动:

public class MainActivity extends AppCompatActivity {

    private DrawerLayout drawer_layout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        android.support.v7.widget.Toolbar toolbar = (android.support.v7.widget.Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        NavigationView nav_view = (NavigationView) findViewById(R.id.nav_view);  //
        drawer_layout = (DrawerLayout) findViewById(R.id.drawer_layout);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
           }

        nav_view.setCheckedItem(R.id.nav_call);    //
        nav_view.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                drawer_layout.closeDrawers();
                return true;
            }
        });   //
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.toolbar, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case android.R.id.home:
                drawer_layout.openDrawer(GravityCompat.START);
                break;

            case R.id.backup:
                Toast.makeText(this, "Backup",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;
                default:
        }
    return true;
    }

}

 获得NavigationView 实例后调用它的setCheckedItem()方法 将Call菜单设置为默认选中。  接着调用 setNavigationItemSelectedListener()方法 来设置一个菜单项选中事件的监听器, 当用户点击任意菜单项时, 就会回调到onNavigationItemSelected() 方法中。 可以在这个方法中写出相应的逻辑处理, 此处只是将滑动菜单关闭。

Material Design实战_第3张图片

3、悬浮按钮和可交互提示

   立体设计是Material Design 中一条非常重要的设计思想, 这种按钮不属于主界面平面的一部分, 而是位于另外一个维度的, 因此给人以悬浮感。 关于提示工具, 我们经常使用Toset, 但是Toast不能与用户交互, 只是起到有个通知的作用。

  (1)、FloatingActionButton

修改主活动布局:



    
        
        
    


    

layout_gravity属性指定将这个控件放置于屏幕的右下角, end的原理即 如果系统语言是从左到右, 那么end就表示右边。 

public class MainActivity extends AppCompatActivity {

    private DrawerLayout drawer_layout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        android.support.v7.widget.Toolbar toolbar = (android.support.v7.widget.Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        NavigationView nav_view = (NavigationView) findViewById(R.id.nav_view);
        drawer_layout = (DrawerLayout) findViewById(R.id.drawer_layout);
        ActionBar actionBar = getSupportActionBar();
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); //

       fab.setOnClickListener(new View.OnClickListener() {   //
           @Override
           public void onClick(View v) {
               Toast.makeText(MainActivity.this, "FAB clicked", Toast.LENGTH_SHORT).show();
           }
       });


        ......

}

 FloatingActionBar 其实和普通Button 差不多, 都是调用setOnClickListener()方法来注册一个监听器。

Material Design实战_第4张图片

   (2)、Snackbar

Design Support 库提供的更加先进的提示工具----Snackbar。 Snackbar并不是Toast的替代品, Snackbar允许在提示当中鸡加入一个可交互按钮, 当用户点击按钮时可以执行一些额外的逻辑操作。 比如当我们删除一些数据时, 添加一个Undo 按钮, 就相当于给用户提供了一种弥补措施, 从而提示了用户的使用体验。

  Snackbar用法与Toast 相似, 只不过可以额外增加一个按钮的点击事件。 修改主活动:

 

fab.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               
               Snackbar.make(v, "Data delted", 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();
               
           }
       });

这里调用了Snackbar 的 make()方法来创建一个Snackbar对象, make() 方法的第一个参数需要传入一个View, 只要是当前界面布局的任意一个View都可以, Snackbar 会使用这个View 来自动来查找最外层的布局, 用于展示Snackbar。 第二个参数就是Snackbar 中显示的内容, 第三个参数是Snackbar 显示的时长。   接着调用setAction() 从而让Snackbar 不仅仅是一个提示, 而是可以和用户进行交互的。 最后调用show() 让Snackbar显示出来。

  Material Design实战_第5张图片Material Design实战_第6张图片

 注意此处有一个bug, Snackbar竟然将悬浮按钮挡住了, 此时我们可以借助 CoordinatorLayout 就可以轻松解决。 

 (3)、CoordinatorLayout

CoordinatorLayout 可以说是一个加强版的FrameLayout, 也是由Design Support库提供的, 其必然就一些Material Design的魔力。 事实上 CoordinatorLayout 可以监听其所有子控件的各种事件, 然后帮我们做出合理的响应。 接下来我们使用以下CoordinatorLayout 。 只需要将原来的 FrameLayout 替换一下即可, 修改主活动布局:




    
        
        
        
        
    


    

 Material Design实战_第7张图片

可以看到悬浮按钮随着Snackbar上下浮动了, 解决了上面的bug。

  

CoordinatorLAyout可以监听其所有子控件的各种事件, 但是Snackbar 并不是其控件, 但它为啥会被监听的呢? 其道

理是我们在Snackbar 的 make()方法中传入的第一个参数, 这个参数是用来指定Snackbar是基于那个View来触发的, 刚才传入的是FloatingActionButton 本身, 而FloatingActionButton 是 CoordinatorLayout 中的子控件, 因此这个事件 就能被监听到了。 

4、卡片式布局

  为了让主体内容也Material化, 准备在这放一些水果图片, 本节中我们将会学习如实现卡片式布局的效果。  卡片式布局也是

Materials Design中提到的一个新的概念, 它可以让页面中的元素看起来就像在卡片中一样, 并且还能拥有圆角和投影。

  (1)、CardView

   CardView是实现卡片式布局的重要控件, 由v7库提供。 实际上, CardView也是一个FrameLayout,  只是额外提供了圆角和阴影等效果, 看上去有立体的效果。

   接下来我们使用RecyclerView 来填充项目的主界面,  由于我们需要用到RecyclerView, CardView 这几个控件, 因此必须在gradle中声明这些库的依赖:

    

implementation 'com.android.support:recyclerview-v7:26.1.0'
    implementation 'com.android.support:cardview-v7:26.1.0'
    implementation 'com.github.bumptech.glide:glide:3.7.0'

  最后一行添加了 Glide库。 Glide是一个超级强大的图片加载库, 他不仅可以用于加载本地图片, 还可以加载网络图片、 GIF图片、 甚至是本地视频。 最重要的是, Glide的用法非常简单, 只需要一行代码就能实现复杂图片的加载。 

  接下来开始具体的代码实现, 修改主活动布局:



    

        
        
        
        
        
        

    
    

      这里我们在CoordinatorLayout 中添加了一个RecyclerView, 给它指定一个id, 然后将宽高占满布局的空间。

  接着定义一个实体类Fruit, 代码如下:

 

public class Fruit {
    private String name;
    private int imageId;
    
    public Fruit(String name, int imageId){
        this.name = name;
        this.imageId = imageId;
    }

    public String getName() {
        return name;
    }

    public int getImageId() {
        return imageId;
    }
}

name代表水果名字, imageId 代表图片资源id。 然后需要为RecyclerView 的子项指定一个我们自定义的布局, 在layout目录下新建 fruit_item.xml:




    

        

        
    

   CardView来作为子项的最外层布局, 从而使得RecyclerView 中的每个元素都是在卡片中的。 CardView 由于是一个FrameLayout, 因此他没有什么方便地定位方式, 在这我们在其嵌套一个LinearLayout。 注意ImageView, 这里使用了centerCrop 模式, 它可以让图片保持原有比例充满 ImageView , 并将超出部分裁减掉。

    接下来为 RecyclerView 准备一个适配器, 新建一个FruitAdapter 类, 让这个适配器去继承RecyclerView.Adapter, 并将泛型指定为 FruitAdapter.ViewHolder, 其中,ViewHolder是我们在FruitAdapter 中定义的一个内部类:

public class FruitAdapter extends RecyclerView.Adapter {

    private Context mContext;

    private  List mFruitList;
    
    @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);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruit_name.setText(fruit.getName());
        Glide.with(mContext).load(fruit.getImageId()).into(holder.fruit_image);
     }

    @Override
    public int getItemCount() {
        return mFruitList.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        CardView cardView;
        ImageView fruit_image;

        TextView fruit_name;

        public ViewHolder(View itemView) {
            super(itemView);
            cardView = (CardView) itemView;
            fruit_name = (TextView) itemView.findViewById(R.id.fruit_name);
            fruit_image = (ImageView) itemView.findViewById(R.id.fruit_image);
        }
    }
        public FruitAdapter(List fruitList) {

            mFruitList = fruitList;

        }
    
}

   代码虽长, 但并不难理解, 这里首先自定义了一个内部类 ViewHolder, ViewHolder 要继承自RecyclerView.ViewHolder。 然后ViewHolder 的构造函数中要传入一个View参数, 这个参数就是RecyclerView 子项的最外层布局, 那么我们就可以提供findViewById()方法获取布局中的ImageView 和 TextView 的实例。

    接着看FruitAdapter 中也有一个构造函数, 这个方法用于把要展示的数据源传进来, 并赋值给一个全局变量mFruitList, 后续的操作都将在这个数据源的基础上进行。

   继续看, 由于FruitAdapter 是继承自RecyclerView.Adapter 的, 那么就必须重写onCreateViewHolder()、 onBindViewHolder()、 和 getItemCount() 这3个方法。 onCreateViewHolder() 方法是用来创建ViewHolder实例的, 在这个方法中将 fruit_item 布局加载进来, 然后创建一个ViewHolder实例, 并把加载的布局传入到 构造函数中, 最后返回ViewHolder实例,    onBindViewHolder() 方法是用于对RecyclerView子项数据进行赋值的,  在每个子项被滚到屏幕是被执行, 通过position 参数得到当前Fruit, 然后将数据设置到 ViewHolder 的 ImageView 和 TextView当中即可。 getItemCount() 直接返回数据源的长度。

  在onBindViewHolder()方法中我们使用了Glide 来加载图片。 其用法为 Glide.with()方法传入一个 Context, Activity或 Fragment 参数, 然后调用 load()方法去加载图片, 可以是本地地址, 也可以是一个URL 地址, 或者是一个资源id, 最后调用into() 将图片设置到一个具体的ImageView 中即可。 

  Glide可以将高清图片进行压缩, 不会造成内存溢出比传统方式更好。

 如此, 适配器就创建完成, 最后修改主活动:

public class MainActivity extends AppCompatActivity {

    private DrawerLayout drawer_layout;

    private Fruit[] fruits = {new Fruit("Apple", R.drawable.apple), new Fruit("Banana", R.drawable.banana),
    new Fruit("Orange", R.drawable.orange), new Fruit("Watermelon", R.drawable.watermelon),
    new Fruit("Pear", R.drawable.pear), new Fruit("Grape", R.drawable.grape), new Fruit("Pineapple", R.drawable.pineapple),
    new Fruit("Strawberry", R.drawable.strawberry), new Fruit("Cherry", R.drawable.cherry), new Fruit("Mango", R.drawable.mango)
    };

    private List fruitList = new ArrayList<>();

    private FruitAdapter adapter;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        android.support.v7.widget.Toolbar toolbar = (android.support.v7.widget.Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        initFruits();
        RecyclerView recycler_view = (RecyclerView) findViewById(R.id.recycler_view);
        GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
        recycler_view.setLayoutManager(layoutManager);
        adapter = new FruitAdapter(fruitList);
        recycler_view.setAdapter(adapter);

        NavigationView nav_view = (NavigationView) findViewById(R.id.nav_view);
        drawer_layout = (DrawerLayout) findViewById(R.id.drawer_layout);
        ActionBar actionBar = getSupportActionBar();
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);

       fab.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {

               Snackbar.make(v, "Data delted", 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();

           }
       });


        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
           }

        nav_view.setCheckedItem(R.id.nav_call);
        nav_view.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                drawer_layout.closeDrawers();
                return true;
            }
        });
    }

    private void initFruits() {

        fruitList.clear();
        for(int i = 0; i < 50; i++){
            Random random = new Random();
            int index = random.nextInt(fruits.length);
            fruitList.add(fruits[index]);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.toolbar, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case android.R.id.home:
                drawer_layout.openDrawer(GravityCompat.START);
                break;

            case R.id.backup:
                Toast.makeText(this, "Backup",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;
                default:
        }
    return true;
    }

}

在数组中存放了多个Fruit 实例, 然后在initFruits() 方法中, 先是清空了一下 fruitList 中的数据, 接着使用一个随机函数 从数组中挑一个水果放入fruitList中, 为了让数据更丰富 使用了一个循环, 随机挑选50个水果。

GridLayoutManager布局方式的用法也没什么特别之处,它的构造函数接收两个参数, 第一个是Context, 第二个是列数, 这里我们希望每行中有两列数据。 

Material Design实战_第8张图片

这里可以发现, ToolBar 被RecyclerView挡住了,  需要使用 AppBarLayout 解决这个问题。

(3)、AppBarLayout

   RecyclerView 和 Toolbar 都是放置在CoordinatorLayout 中的, 由于 CoordinatorLayout是一个加强版的 FrameLayout , 那么FrameLayout 中所有的控件在不进行明确定位的情况下, 默认都摆放在左上角, 从而产生了遮挡。 这就是Toolbar 被 RecyclerView 遮挡的原因。

  传统的情况下我们会使 RecyclerView 向下偏移一个ToolBar 的高度, 然而我们 的是CoordinatorLayout , 我们可以抛弃传统方式, 而使用 Design Support 库中提供的另外一个工具---AppBarLayout。 AppBarLayout实际上是一个垂直方向的 LinearLayout , 它在内部做了很多滚动事件的封装, 并应用的 Material Design的设计理念。 

   解决覆盖问题, 只需要两步, 第一 将Toolbar 嵌套到 AppBarLayout中, 第二 给 RecyclerView 指定一个布局行为。 修改主活动布局:

...


        

        

        

        
        
...

 接下来我们来看看 AppBarLayout 到底能实现什么样的 Material Design效果。 接收到滚动事件的时候, 它内部的子控件其实是可以指定如何去影响这些事件的, 通过 app:layout_scrollFlags 属性就能实现。 修改主活动布局:

...

...

此处给ToolB中添加了app:layout_scrollFlags" 属性, 并赋值scroll|enterAlways|snap, 其中 scroll 表示当 RecyclerView 向上滚动的时候 Toolbar 会一起向上滚动并实现隐藏, enterAlways 表示当RecyclerView 向下滚动时, Toolbar 会一起向下滚动 并重新显示, snap 表示当Toolbar 还没有完全隐藏或显示 的时候, 会根据当前滚动的距离, 自动选择显示还是隐藏。

Material Design实战_第9张图片

 像这种功能, 是ActionBar无法实现的, ToolBar为我们提供了更多可能。

5、下拉刷新

 谷歌为了让Android的下拉刷新风格能有一个统一的标准, 于是在Material Design 中定制了一个官方的设计规范, 谷歌早就提供了现成的控件, 我们只需在项目中直接引用即可。 

  SwipRefreshLayout 就是用于实现下拉刷新的核心类, 由 support-v4 库提供的。 我们把控件放置到SwipRefreshLayout 中, 就可以迅速让这个控件实现下拉刷新。 在这个Test中, 支持下拉刷新的控件自然就是 RecyclerView 了, 修改主活动的布局:

 

...
 
        
        
        

        
...

  如此RecyclerView 就具有下拉刷新功能了, 另外注意, 由于RecyclerView 现在变成了SwipRefreshLayout 的子控件, 因此之前使用app:layout_behavior 声明的布局行为现在也要移到 SwipRefreshLayout 中才行。

  接下来需要在代码中实现具体的刷新逻辑。 修改主活动代码:

  

public class MainActivity extends AppCompatActivity {

    private DrawerLayout drawer_layout;

    private Fruit[] fruits = {new Fruit("Apple", R.drawable.apple), new Fruit("Banana", R.drawable.banana),
    new Fruit("Orange", R.drawable.orange), new Fruit("Watermelon", R.drawable.watermelon),
    new Fruit("Pear", R.drawable.pear), new Fruit("Grape", R.drawable.grape), new Fruit("Pineapple", R.drawable.pineapple),
    new Fruit("Strawberry", R.drawable.strawberry), new Fruit("Cherry", R.drawable.cherry), new Fruit("Mango", R.drawable.mango)
    };

    private List fruitList = new ArrayList<>();

    private FruitAdapter adapter;
    
    private SwipeRefreshLayout swipeRefreshLayout;//
    private SwipeRefreshLayout swipe_refresh;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        android.support.v7.widget.Toolbar toolbar = (android.support.v7.widget.Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        initFruits();
        RecyclerView recycler_view = (RecyclerView) findViewById(R.id.recycler_view);
        GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
        recycler_view.setLayoutManager(layoutManager);
        adapter = new FruitAdapter(fruitList);
        recycler_view.setAdapter(adapter);

        NavigationView nav_view = (NavigationView) findViewById(R.id.nav_view);
        drawer_layout = (DrawerLayout) findViewById(R.id.drawer_layout);

        swipe_refresh = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh);
        swipe_refresh.setColorSchemeResources(R.color.colorPrimary);
        swipe_refresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                refreshFruits();
            }
        });
        
        ActionBar actionBar = getSupportActionBar();
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);

        fab.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {

               Snackbar.make(v, "Data delted", 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();

           }
       });


        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
           }

        nav_view.setCheckedItem(R.id.nav_call);
        nav_view.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                drawer_layout.closeDrawers();
                return true;
            }
        });
    }

    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();
                        swipe_refresh.setRefreshing(false);
                        
                    }
                });
            }
        }).start();
    }

...
}

   此处通过 findViewById() 方法获得实例, 然后调用 setColorSchemaResources() 方法来设置进度条颜色, setOnRefreshListener() 方法设置一个下拉刷新的监听器, 触发刷新时就会回调这个监听器 的onRefresh() 方法, 在这里可以去处理刷新逻辑。通常在是去网络请求最新数据,再将数据展示出来。 我们这就展示一个刷新本地图片操作, refreshFruits() 方法中 开启一个线程, 然后将线程沉睡两秒, 沉睡结束后 使用runOnUiThread() 方法将线程切换到主线程, 然后调用 initFruits() 重新生成数据, 接着调用FruitAdapter 的 notifyDataSetChanged() 方法通知数据发送了变化, 最后调用 SwipRefreshLayout 的 setRefreshing() 方法并传入false , 用于表示刷新结束, 并隐藏进度条。

   Material Design实战_第10张图片


6、可折叠式标题栏

  在Material Design中可以根据我们的喜好指定标题栏的样式, 比如折叠式标题栏。 需要借助 CollapsingToolbarLayout 这个工具。

(1)、CollapsingToolbarLayout 

   CollapsingToolbarLayout 是一个可以作用于 ToolBar 基础之上的布局, 由Design Support 库 CollapsingToolbarLayout 
 可以让Toolbar 的效果更佳丰富。 不过它是不可以独立存在的, 在设计时就被限定只能作为 AppBarLayout 的直接子布局来使用。 而 AppBarLayout 又必须是 CoordinatorLayout  的子布局, 因此让我们这里的项目是个布局综合性蛮强的操作。 

  创建一个FruiteActivity, 开始编写水果内容详情, 我们来一步步实现。


 

  注意始终定义一个 xmlns:app的命名空间, 在Material Design 的开发会经常用到它。 

  接着在CoordinatorLayout 中嵌套一个AppBarLayout, 如下:


    
    
    

接着在AppBarLayout 中再嵌套一个CollapsingToolbarLayout, 如下所示:

...


        
            
        

    
...

  这里我们使用了新布局CollapsingToolbarLayout, android:theme 属性用于指定 CollapsingToolbarLayout 在趋于折叠状态以及折叠之后的背景色, 其实CollapsingToolbarLayout 折叠后就是一个普通的 Toolbar , 背景肯定是 colorPrimary 了。 

app:layout_scrollFlags 属性我们也是见过的, 之前 Toolbar 见过,  scroll 表示CollapsingToolbarLayout 会随着水果内容详情的滚动一起滚动, exitUnitilCollapsed 表示当CollapsingToolbarLayout 随着滚动完成折叠之后就保留在界面,不在移除屏幕。 

   接下来, 我们在 CollapsingToolbarLayout 中定义标题栏内容, 如下:

...

            
            
            
            
            

        
...

此处, 我们在 CollapsingToolbarLayout  中定义了一个 ImageView 和一个 Toolbar, 也就意味着, 这个高级版的标题将是

由普通标题栏 加上图片组成而成的。 这里有个app:layout_collapseMode 比较陌生。 它用于指定当前控件在 CollapsingToolbarLayout 折叠过程中的折叠模式, 其中Toolbar 指定为pin ,表示在折叠过程中位置始终保持不变, ImageViwe 指定成 parallax, 表示会在折叠的过程产生一定的错位漂移, 这种模式视觉效果好。

  这样水果标题栏就完成了, 下面编写水果内容详情部分:



    

       ......

    

    

    

   水果内容详情的最外层布局使用了一个 NestedScrollView, 注意它和AppBarLayout 是平级的。  ScrollView 的用法, 它允许

使用滚动的方式来查看屏幕外的数据, 而NestedView 在此基础之上还增加了嵌套响应滚动时间的功能。 由于CoordinatorLayout 本身已近可以响应滚动事件了, 因此我们在它的内部就需要使用 NestedScrollView 或 RecyclerView 这样的布局。 另外, 这里还通过 app: layout_behavior 属性指定了一个布局行为, 这和之前在 RecyclerView 中的用法是一模一样的。

   不管是ScrollerView 还是 NestedScrollerView, 它们的内部都只允许一个直接子布局。 因此我们想要在里面放很多东西的话, 通常都会嵌套一个 LinearLayout , 然后在 LinearLayout 中放入具体的内容即可。在这里我们将准备使用一个 TextView来显示水果的内容详情, 并将 TextView 放在一个卡片式布局当中:

   

...

        
        
            
            
          
              
            
         

    
...

  在CardView 和 TextView 上都加了一些边距。 CardView的 marginTop 加了35dp的边距, 为了下面要编写的东西留出空间。

  



    

        

            

            
            

        

    

    

    


        

        
    


    
    
       

  FloatingActionButton, 和 AppBarLayout、 NestedScrollView 是平级的。 其中, 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 collapsing_toolbar = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
        ImageView fruit_image_view = (ImageView) findViewById(R.id.fruit_image_view);
        TextView fruit_content_text = (TextView) findViewById(R.id.fruit_content_text);

        setSupportActionBar(toolbar);

        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }

        collapsing_toolbar.setTitle(fruitName);
        Glide.with(this).load(fruitImageId).into(fruit_image_view);
        String fruitContent = generateFruitContent(fruitName);
        fruit_content_text.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);
    }
}

   在onCreate() 方法中, 我们通过Intent 获取到传入水果的名 和水果图片的资源ID, 然后获得布局文件中的各个空间实例。  接着就用了 Toolbar的标准用法, 将它作为ActionBar 显示, 并启用HomeAsUp 按钮。 由于HomeAsUp  按钮的默认图标是一个返回按钮, 这正是我们希望的, 因此无需设置别的图标了。

    接下来开始填充界面上的内容, 调用CollapsingToolbarLayout 的 setTitle() 将水果名设置为标题, 然后使用Glide 加载传入的水果图片, 并设置到标题栏的 ImageView 上面。 此处填充了500此水果名。

  接着, 我们在onOptionsItemSelected() 方法中处理了 HomeAsUp 按钮的点击事件, 当点击按钮 就调用finish() 方法关闭此活动, 从而返回上一个活动。

 最后一步, 处理RecyclerView 的点击事件, 不然就无法打开FruitActivity, 修改FruitAdapter:

  

public class FruitAdapter extends RecyclerView.Adapter {

    private Context mContext;

    private  List mFruitList;

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (mContext == null) {
            mContext = parent.getContext();
        }

        View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item, parent, false);
        
        
        final ViewHolder holder = new ViewHolder(view);     //
        holder.cardView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = holder.getAdapterPosition();
                Fruit fruit = mFruitList.get(position);
                Intent intent = new Intent(mContext, FruitActivity.class);
                intent.putExtra(FruitActivity.FRUIT_NAME, fruit.getName());
                intent.putExtra(FruitActivity.FRUIT_IMAGE_ID,fruit.getImageId());
                mContext.startActivity(intent);
            }
        });    //

        return holder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruit_name.setText(fruit.getName());
        Glide.with(mContext).load(fruit.getImageId()).into(holder.fruit_image);
     }

    @Override
    public int getItemCount() {
        return mFruitList.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        CardView cardView;
        ImageView fruit_image;

        TextView fruit_name;

        public ViewHolder(View itemView) {
            super(itemView);
            cardView = (CardView) itemView;
            fruit_name = (TextView) itemView.findViewById(R.id.fruit_name);
            fruit_image = (ImageView) itemView.findViewById(R.id.fruit_image);
        }
    }
        public FruitAdapter(List fruitList) {

            mFruitList = fruitList;

        }

}

  在这里, 我们给CardView 注册了一个点击事件监听器, 然后在点击事件当前点击项的水果名和图片资源id,把他们传入到intent 中, 最后调用startActivity() 启动FruitActivity。

 (2)、充分利用系统状态栏空间

 虽说水果详情页展示界面的效果已经非常华丽的, 但这并不代表我们不能再进一步地提高升。 其实这里我们能将背景图和状态栏融合到一起, 那这个视觉体验绝对提升几个档次。 但是再Android5.0之前, 是无法对状态栏的背景或颜色进行操作的, 那个时候也没有 Material Design 的概念。 因此在Android 5.0 系统之后我们完全可以 使用背景图和状态栏融合的模式。 

  此时需要借助 android:fitsSystemWindows 这个属性来将其属性设置为true , 这就表示控件会出现在系统状态栏里。在这里 就是水果标题栏中的 ImageView 应该设置这个属性了。 但是之给 ImageView设置此属性是不够的, 必须将ImageView 布局结构中的所有父布局都设置上这个属性才行, 修改acticity_fruit.xml:

  



    

        

            
......

   但是, 只设置android:fitsSystemWindows 属性都设置好了是没有的, 因为还必须在程序的主题中将状态指定为透明色才行。 只要将主题将 android: statusBarColor 属性的值指定成 @android:color/transparent 就可以了。 但问题是, android:statusBar 这个属性是从API21, 也就是Android5.0 系统开始才有的, 之前的系统无法指定这个属性。 

 那么。。。

系统差异型功能实现就要从这里开始了。。。

  右击res目录, 新建一个value-v21目录, 创建一个sytle.xml 文件:


    @android:color/transparent

    在这里我们定义了一个FruitActivityTheme 主题, 它是专门给 FruitActivity使用的。 FruitActivityTheme 的 parent 主题是AppTheme, 也就是说它继承了 AppTheme的所有特性。 然后我们在 FruitActitityTheme主题 将状态栏颜色设置为透明, 且只有 Android5.0 及以上系统才会去读取。   

  但是再Android5.0 之前的系统时无法对其进行识别的, 因此我们还需要对 values/styles.xml文件进行修改:

 



    
    
    
       //

 可以看到这里定义了一个 FruitActivityTheme 主题,并且parent 主题也是 AppTheme, 但是它的内部是空的。 因为 Android5.0 之前的系统无法指定系统状态栏的颜色, 因此这里什么都不用做就可以了。

  最后,我们还需要让 FruitActivity 使用这个主题才可以, 修改清单:

...
   //
        
        
...

 这里使用android:theme 属性单独给FruitActivity 指定了FruitActivityTheme 这个主题, 如此即大功告成。

 现在只要是在Android5.0 及以上系统运行 MaterialTest 程序, 水果详情展示界面的效果就会如图:

Material Design实战_第11张图片


结束!!!



  


  

  



   



你可能感兴趣的:(Android项目实战记录)