您现在的位置是:首页 > 文章详情

Material Design 实战 之第四弹 —— 卡片布局以及灵动的标题栏(CardView & AppBarLayout)

日期:2018-10-01点击:364

本模块共有六篇文章,参考郭神的《第一行代码》,对Material Design的学习做一个详细的笔记,大家可以一起交流一下:





卡片式布局也是MaterialsDesign中提出的一个新的概念,它可以让页面中的元素看起来就像在卡片中一样,并且还能拥有圆角和投影,下面我们就开始具体学习一下。

img_56ac76a5c6a1e68367192055e77c4fb1.png
最终成果图



文章提要与总结


1. CardView(这里用于作为recycleview的子项,用于显示水果) 1.1 实际上,CardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉; 1.2 app:cardCornerRadius属性指定卡片圆角的弧度,数值越大,圆角的弧度也越大; app:elevation属性指定卡片的高度, 高度值越大,投影范围也越大,但是投影效果越淡, 高度值越小,投影范围也越小,但是投影效果越浓, FloatingActionButton同理。 
 1.3 需要依赖: compile 'com.android.support:cardview-v7:25.3.1' 本项目还需添加一个Glide库的依赖。 compile 'com.github.bumptech.glide:glide:3.7.0' Glide是一个超级强大的图片加载库,它不仅可以用于加载本地图片, 还可以加载网络图片、GIF图片、甚至是本地视频。 最重要的是,Glide的用法非常简单,只需一行代码就能轻松实现复杂的图片加载功能; 
 1.4 在toolbar下面添加一个recycleview 定义一个实体类Fruit,方便后面存取数据; 为RecycleView的子项制定一个自定义布局(架构如下): 
 <android.support.v7.widget.CardView <LinearLayout <ImageView/> <TextView/> </LinearLayout> </android.support.v7.widget.CardView> 
 接下来需要为RecyclerView准备一个适配器, 适配器中除了RecycleView的设计逻辑之外,这里需要注意的是, 在onBindViewHoIder()方法中使用Glide来加载水果图片。 Glide的用法: 首先调用Glide.with()方法并传入一个Context、Activity或Fragment参数; 然后调用load()方法去加载图片,其参数可以是一个URL地址 或 本地路径 或 资源id; 最后调用into()方法将图片设置到具体某一个ImageView中即可。 1.5 在MainActivity中: 初始化水果列表; 实例化recyclerView ; newLayoutManager & set; new & set adapter; 2.AppBarLayout 2.1 将Toolbar嵌套到AppBarLayout中; 2.2 给RecyclerView指定一个布局行为(app:layout_behavior)——appbar_scrolling_view_behavior 
 2.3 在Toolbar中添加一个app:layout_scrollFlags属性,并其值指定成了scroll|enterAlways|snap。 其中, scroll 表示当RecyclerView向上滚动时,Toolbar会跟着一起向上滚动并实现隐藏; enterAlways 表示当RecyclerView向下滚动时,Toolbar会跟着一起向下滚动并重新显示; snap 表示当Toolbar还没有完全隐藏或显示时,会根据当前滚动的距离,自动选择是隐藏还是显示。 
img_a8d0632079c41c7c29cf45a9c895f8ba.png
效果图




正文


CardView

首先这里准备用CardView来填充主题内容,
CardView是用于实现卡片式布局效果的重要控件,由appcompat-v7库提供。
实际上,CardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉。

CardView 的基本用法:

<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"> <TextView android:id="@+id/fruit_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_margin="5dp" android:textSize="16sp"/> </android.support.v7.widget.CardView> 

其中:
app:cardCornerRadius属性指定卡片圆角的弧度,数值越大,圆角的弧度也越大;
app:elevation属性指定卡片的高度,
高度值越大,投影范围也越大,但是投影效果越淡,
高度值越小,投影范围也越小,但是投影效果越浓, FloatingActionButton同理。

然后我们在CardView布局中放置了一个TextView,这个TextView就会显示在一张卡片中了。

为充分利用屏幕的空间,我们可以使用RecyclerView来填充MatenalTest项目的主界面部分。
这里参考一下郭神的demo——实现水果列表,首先需要准备许多张水果图片:


img_1f1341b611f2dbe712be5bd31a76b78b.png



然后在app/build.gradle文件中声明RecyclerView、CardView这几个控件对应的库的依赖:

 compile 'com.android.support:recyclerview-v7:25.3.1' compile 'com.android.support:cardview-v7:25.3.1' 

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

接下来修改activity-main.xml,如下所示(在toolbar下面添加一个recycleview),

<?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@drawable/ic_done" app:elevation="8dp"/> </android.support.design.widget.CoordinatorLayout> <android.support.design.widget.NavigationView android:id="@+id/nav_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" app:menu="@menu/nav_menu" app:headerLayout="@layout/nav_header"> </android.support.design.widget.NavigationView> </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 int getImageId() { return imageId; } } 

类中就两个字段,
name对应水果的名字;
imageId对应图片的资源id。

接下来需要为RecycleView的子项制定一个自定义布局。在layout目录下新建fruit_item.xml:

<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="5dp" app:cardCornerRadius="4dp"> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/fruit_image" android:layout_width="match_parent" android:layout_height="100dp" android:scaleType="centerCrop"/> <TextView android:id="@+id/fruit_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_margin="5dp" android:textSize="16sp"/> </LinearLayout> </android.support.v7.widget.CardView> 

这里使用了CardView来作为子项的最外层布局,从而使得RecyclerView中的每个元素都是在卡片当中的。

CardView由于是一个FrameLayout,因此它没有什么方便的定位方式,这里只好在CardView中再嵌套一个LinearLayout,然后在LinearLayout中放置具体的内容。

内容的话就是
定义了ImageView用于显示水果的图片,
定义了TextView用于显示水果的名称,并让TextView在水平方向上居中显示。

注意在ImageView中我们使用了一个scaleType属性,这个属性可以指定图片的缩放模式。
由于各张水果图片的长宽比例可能都不一致,为了让所有的图片都能填充满整个ImageView,这里使用了centerCrop模式,它可以让图片保持原有比例填充满ImageView,并将超出屏幕的部分裁剪掉。

接下来需要为RecyclerView准备一个适配器
新建FruitAdapter类继承RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder,代码如下(具体可见代码中注释):

img_5072f499d93bfe75b21ae955968d6b0b.png
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> { private Context mContext; private List<Fruit> mFruitList; //实例化子项布局各个view对象 static class ViewHolder extends RecyclerView.ViewHolder{ CardView cardView; ImageView fruitImage; TextView fruitName; public ViewHolder(View view){ super(view); cardView = (CardView) view; fruitImage = (ImageView) view.findViewById(R.id.fruit_image); fruitName = (TextView) view.findViewById(R.id.fruit_name); } } public FruitAdapter(List<Fruit> fruitList){ mFruitList = fruitList; } //加载子布局,将子项作为参数传给ViewHolder,在ViewHolder里面 @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if(mContext == null){ mContext = parent.getContext(); } View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item, parent, false); return new ViewHolder(view);//将子项作为参数传给ViewHolder,在ViewHolder里面面实例化子项中的各个对象 } //set对应子项对象 @Override public void onBindViewHolder(ViewHolder holder, int position) { Fruit fruit = mFruitList.get(position);//get对应子项对象 holder.fruitName.setText(fruit.getName()); Glide.with(mContext).load(fruit.getImageId()).into(holder.fruitImage); } @Override public int getItemCount() { return mFruitList.size(); } } 

除了RecycleView的设计逻辑之外,这里需要注意的是,在onBindViewHoIder()方法中使用Glide来加载水果图片。

Glide的用法:

  • 首先调用Glide.with()方法并传入一个Context、Activity或Fragment参数;
  • 然后调用load()方法去加载图片,其参数可以是一个URL地址/本地路径/资源id;
  • 最后调用into()方法将图片设置到具体某一个ImageView中即可。

这里使用Glide而不是传统的设置图片方式:
因这里从网上找的这些水果图片像素都非常高,如果不进行压缩直接展示,很容易就会引起内存溢出。
而使用Glide就完全不需要担心这回事,因为Glide在内部做了许多非常复杂的逻辑操作,
其中就包括了图片压缩,只需要安心按照Glide的标准用法去加载图片就可以了。

这样RecyclerView的适配器便准备好了,最后修改MainActivity中的代码:

img_fbcfa0451d6d8a633d78dda1059af8e2.png

img_46290cb2417df718d143fca009d4a9ba.png

img_16da5f3d20aa0c65778362f438e8eada.png

public class MainActivity extends AppCompatActivity { private DrawerLayout mDrawerLayout; //增加RecycleView后的数据和对象初始化 private Fruit[] fruits = {new Fruit("Apple", R.drawable.apple),new Fruit("Banana", R.drawable.banana), new Fruit("Orange", R.drawable.orange),new Fruit("Watermelon", R.drawable.watermelon), new Fruit("Pear", R.drawable.pear),new Fruit("Grape", R.drawable.grape), new Fruit("Pineapple", R.drawable.pineapple),new Fruit("Strawberry", R.drawable.strawberry), new Fruit("Cherry", R.drawable.cherry),new Fruit("Mango", R.drawable.mango)}; private List<Fruit> fruitList = new ArrayList<>(); private FruitAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); //滑动菜单 & 导航按钮 mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); NavigationView navView = (NavigationView) findViewById(R.id.nav_view); ActionBar actionBar = getSupportActionBar(); if(actionBar != null){ actionBar.setDisplayHomeAsUpEnabled(true);//让导航按钮显示出来 actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);//设置一个导航按钮图标 } //滑动菜单布局交互设置 navView.setCheckedItem(R.id.nav_call);//将Call菜单项设置为默认选中 navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener(){ @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { mDrawerLayout.closeDrawers();//关闭滑动菜单 return true; } }); //悬浮按钮点击事件 FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Toast.makeText(MainActivity.this, "FAB clickes", Toast.LENGTH_SHORT).show(); //Snackbar Snackbar.make(v,"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(); } }); initFruits(); //实例化 RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); //newLayoutManager & set GridLayoutManager layoutManager = new GridLayoutManager(this, 2); recyclerView.setLayoutManager(layoutManager); //new & set adapter adapter = new FruitAdapter(fruitList); recyclerView.setAdapter(adapter); } //初始化水果列表 private void initFruits(){ fruitList.clear(); for (int i = 0; i < 50; i++){ Random random = new Random(); int index = random.nextInt(fruits.length);//nextInt()作用:产生[0,fruits.length)之间的int数 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: 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; } } 

代码简析:

  • 在MainActivity中定义了一个数组,数组存放多个Fruit的实例,每个实例代表一种水果;
  • 在initFruits()方法中,先清空fruitList中的数据,再使用一个随机函数,从刚才定义的Fruit数组中随机挑选一个水果放入到fruitList当中,这样每次打开程序看到的水果数据都会是不同的。
    另外,为了让界面上的数据多一些,这里使用了一个循环,随机挑选50个水果。
  • 之后是RecyclerView的逻辑,这里使用GridLayoutManager布局方式。
    GridLayoutManager的构造函数接收两个参数,第一个是Context,第二个是列数,这里指定为2,表示每一行中会有两列数据。

运行效果如图:

img_bd602bb27c15afb884605012ed377110.png

可见Toolbar被挡住了,不急,接下来学习另外一个工具——AppBarLayout,完美解决这个问题。



AppBarLayout


首先RecyclerView会把Toolbar给遮挡住的原因:
由于RecyclerView和Toolbar都是放置在CoordinatorLayout中的,
而前面已经说过,CoordinatorLayout就是一个加强版的FrameLayout,
而FrameLayout中的所有控件在不进行明确定位的情况下,默认都会摆放在布局的左上角,从而也就产生了遮挡的现象。



解决方法:
传统情况下,使用偏移是唯一的解决办法,
即让RecyclerView向下偏移一个Toolbar的高度,从而保证不会遮挡到Toolbar。
不过这里使用的是DesignSupport库的CoordinatorLayout而不是FrameLayout,自然会有更加巧妙的解决办法。

这里准备使用DesignSupport库中提供的另外一个工具——AppBarLayout。
AppBarLayout实际上是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装,并应用了一MaterialDesign的设计理念。

接下来使用AppBarLayout两步解决前面的覆盖问题:
第一步将Toolbar嵌套到AppBarLayout中,
第二步给RecyclerView指定一个布局行为(app:layout_behavior)。
修改activity_main.xml:

img_18a036ce520da6fd39ce981321689962.png

<?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@drawable/ic_done" app:elevation="8dp"/> </android.support.design.widget.CoordinatorLayout> <android.support.design.widget.NavigationView android:id="@+id/nav_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" app:menu="@menu/nav_menu" app:headerLayout="@layout/nav_header"> </android.support.design.widget.NavigationView> </android.support.v4.widget.DrawerLayout> 

改动后布局文件并没有太大变化:
首先定义一个AppBarLayout,并将Toolbar放置在AppBarLayout里面;
然后在RecyclerView中使用app:layout_behavior属性指定一个布局行为。
其中appbar_scrolling_view_behavior这个字符串也是由DesignSupport库提供的。

重新运行一下程序,可见遮挡问题就此解决了:

img_a8d0632079c41c7c29cf45a9c895f8ba.png

至此AppBarLayout已成功解决RecyclerView遮挡Toolbar的问题,但是这里还并没有体现AppBarLayout中应用的MaterialDesign设计理念,

其实,当RecyclerView滚动的时候就便将滚动事件都通知给AppBarLayout了
(记得刚刚加的app:layout_behavior="@string/appbar_scrolling_view_behavior"吗,看一下这个字符串,顾名思义应该可以看出些端倪,这里可以先抽象理解为这个属性指定了的便是RecyclerView滚动的时候做出的行为),
只是上面的代码还没进行处理而已。

当AppBarLayout接收到滚动事件的时候,它内部的子控件是可以指定如何去影响这些事件的,
通过app:layout_scrollFlags属性就能实现。

下面进一步优化,加一个代码看看AppBarLayout的这个Material Design效果,修改activity-main.xml:

img_c9ae1645fb8c2ba4a9c41860eae5cdf3.png

app:layout_scrollFlags="scroll|enterAlways|snap" 

这里在Toolbar中添加一个app:layout_scrollFlags属性,并其值指定成了scroll|enterAlways|snap。
其中,
scroll表示当RecyclerView向上滚动时,Toolbar会跟着一起向上滚动并实现隐藏;
enterAlways表示当RecyclerView向下滚动时,Toolbar会跟着一起向下滚动并重新显示;
snap表示当Toolbar还没有完全隐藏或显示时,会根据当前滚动的距离,自动选择是隐藏还是显示。

这里要改动的其实也就这一行代码而已,重新运行一下程序,并向上滚动RecyclerView,效果如图:

img_360300ad4036891a4f48fb86a24a6010.png

运行程序可见,
随着我们
向上滚动RecyclerView会Toolbar消失掉;
向下滚动RecyclerView,Toolbar又会重新出现;
滚动到Toolbar的一半时松开手指,Toolbar又会根据当前滚动的距离情况,做出消失或者重新出现的反应;

这其实也是MaterialDesign中的一项重要设计思想,因为当用户在向上滚动RecyclerView的时候,其注意力肯定是在RecyclerView的内容上面的,这个时候如果Toolbar还占据着屏幕空间,就会在一定程度上影响用户的阅读体验,而将Toolbar隐藏则可以让阅读体验达到最佳状态。当用户需要操作Toolbar上的功能时,只需要轻微向下滚动,Toolbar就会重新出现。
这种设计方式,既保证了用户的最佳阅读效果,又不影响任何功能上的操作,Material Design考虑得就是这么细致人微。
当然了,像这种功能,如果是使用ActionBar的话,那就完全不可能实现了,TooIbar的出现为我们提供了更多的可能。

原文链接:https://yq.aliyun.com/articles/686561
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章